<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-9289096</id><updated>2026-10-12T08:30:33.567+08:00</updated><category term="java"/><category term="erlang"/><category term="others"/><category term="programming"/><category term="book"/><category term="scala"/><category term="audio"/><category term="system"/><category term="pattern"/><category term="web"/><category term="rust"/><category term="javascript"/><category term="machine learning"/><category term="database"/><category term="linux"/><category term="elixir"/><category term="python"/><category term="android"/><category term="julia"/><category term="protocol"/><category term="kotlin"/><category term="chart"/><category term="sip"/><category term="RPi"/><category term="lisp"/><category term="animation"/><category term="algorithm"/><category term="radio"/><category term="JWT"/><category term="c#"/><category term="spec"/><category term="swift"/><title type='text'>cctg</title><subtitle type='html'>yaocl in conflict / complication</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default?redirect=false'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default?start-index=26&amp;max-results=25&amp;redirect=false'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>929</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-9289096.post-4094384984351997684</id><published>2026-10-12T08:30:33.567+08:00</published><updated>2026-10-12T08:30:33.567+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>JEP 512 Compact Source Files and Instance Main Methods</title><content type='html'>&lt;p&gt;java 的第一個範例程式 HelloWorld 必須要這樣寫，但初學者不能理解，為什麼一定要寫 class，main method 一定要宣告 public static void，也必須要加上字串陣列參數。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;public class HelloWorld {
    public static void main(String[] args) {
        System.out.println(&quot;Hello, world!&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自 JDK 21 開始，就有幾個簡化入門程式的 preview 功能 JEP，然後到了 JDK 25，正式 final 了 JEP 512。&lt;/p&gt;
&lt;p&gt;主要的改動是，可以不寫 class，可以簡寫 main 為 instance method，也就是不需要宣告 public static void&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;jep-512-核心功能&quot;&gt;JEP 512 核心功能&lt;/h2&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;compact-source-files&quot;&gt;Compact Source Files&lt;/h3&gt;
&lt;p&gt;可以不寫 class&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;public static void main(String[] args) {
    System.out.println(&quot;Hello, world!&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;instance-main-methods&quot;&gt;Instance Main Methods&lt;/h3&gt;
&lt;p&gt;可以去掉 main method 的宣告&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;void main() {
    System.out.println(&quot;Hello, world!&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;支援直接執行&quot;&gt;支援直接執行&lt;/h3&gt;
&lt;p&gt;傳統的 java 程式，必須要先編譯 javac HelloWorld.java 為 HelloWorld.class，然後才能執行 java HelloWorld&lt;/p&gt;
&lt;p&gt;現在可以直接執行 HelloWorld.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;java HelloWorld.java&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;這些功能，並不是 Java 的常態。而僅只是為了讓初學者用類似 python 的腳本式語言的方式，學習並入門 Java。主要用在簡短的教學及範例程式。如果是大型正式的專案，還是不適合使用。&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/4094384984351997684/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/10/jep-512-compact-source-files-and.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/4094384984351997684'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/4094384984351997684'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/10/jep-512-compact-source-files-and.html' title='JEP 512 Compact Source Files and Instance Main Methods'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-2472800761461793672</id><published>2026-09-14T08:34:30.910+08:00</published><updated>2026-09-14T08:34:30.910+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>JEP 467 Markdown Documentation Comments</title><content type='html'>&lt;h1 class=&quot;atx&quot; id=&quot;jep-467-markdown-documentation-comments&quot;&gt;JEP 467 Markdown Documentation Comments&lt;/h1&gt;
&lt;p&gt;自 JDK 23 開始，javadoc 支援在每一行 javadoc 中，以 &lt;code&gt;///&lt;/code&gt; 開頭的方式，標記為markdown 格式的 javadoc，這種方式取代舊的 &lt;code&gt;/** **/&lt;/code&gt; ，並支援 markdown 語法。&lt;/p&gt;
&lt;p&gt;markdown 是簡化的 html，以往在 javadoc，需要用大量的 html tag，標記為&lt;/p&gt;&lt;p&gt; , &lt;/p&gt;&lt;ul&gt;, &lt;code&gt;，現在支援 markdown 語法的 標題、列表、代碼塊、連結、粗體/斜體，讓 javadoc 的內容更精簡。&lt;p&gt;&lt;/p&gt;
&lt;p&gt;新舊兩種註解寫法是同時支援的。新的 markdown，除了比較簡潔以外，解決了傳統的 javadoc 使用 html，太難寫，需要寫很多 html tag 的問題，在查看 code 裡面的註解時，markdown 的可讀性明顯比 html 高。&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;支援的-markdown-語法&quot;&gt;支援的 markdown 語法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;粗體，斜體文字&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;段落與換行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;有序及無序的列表 list&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;程式碼&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;標題&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;連結&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;表格&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;markdown-javadoc-範例&quot;&gt;markdown javadoc 範例&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-markdown&quot;&gt;    /// 這是使用 Markdown 格式的註釋。
    ///
    /// # 範例功能
    /// * 使用 **Highlight** 語法。
    ///
    /// # 程式區塊：
    /// ```java
    /// test(&quot;param&quot;);
    /// ```
    ///
    /// # 列表
    /// 1. 項目1
    /// 2. 項目2
    /// 3. 項目3
    ///
    /// # 連結 [google](&quot;https://www.google.com&quot;)
    /// * 連結
    /// - 模組 [java.base/]
    /// - 套件 [java.util]
    /// - 類別 [String]
    /// - 欄位 [String#CASE_INSENSITIVE_ORDER]
    /// - 方法 [String#chars()]
    /// - 同一類別其他方法: [equals][#equals(Object)]
    ///
    /// # 表格
    ///
    /// | Latin | Greek |
    /// |-------|-------|
    /// | a     | alpha |
    /// | b     | beta  |
    /// | c     | gamma |
    ///
    /// @param param1 參數
    /// @return 回傳值&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;/ul&gt;</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/2472800761461793672/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/09/jep-467-markdown-documentation-comments.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/2472800761461793672'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/2472800761461793672'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/09/jep-467-markdown-documentation-comments.html' title='JEP 467 Markdown Documentation Comments'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-5230391428804839554</id><published>2026-09-07T08:36:50.873+08:00</published><updated>2026-09-07T08:36:50.873+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="pattern"/><title type='text'>SSOT Single Source of Truth</title><content type='html'>&lt;p&gt;單一真理來源、唯一事實來源。資訊系統架構中的一個核心概念。其核心目標在於確保組織內的每個人、每個系統，在需要特定數據時，都參考同一個來源，從而避免數據不一致導致的決策錯誤。&lt;/p&gt;
&lt;p&gt;這樣做可以確保：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;所有資訊的一致性和準確性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;減少重複數據和資訊衝突&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;方便查找與更新&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;簡化備份、遷移和共享流程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;促進團隊協作與溝通&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SSOT 並不是指將「所有數據」都存在同一個資料庫中，而是指對於&lt;strong&gt;某個特定的數據點&lt;/strong&gt;（例如：客戶地址、產品價格、庫存數量），在整個系統中只有一個權威的來源（Master Copy）。&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;在-it-領域的應用意義&quot;&gt;在 IT 領域的應用意義&lt;/h2&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;1-確保數據一致性-data-consistency&quot;&gt;1. 確保數據一致性 (Data Consistency)&lt;/h3&gt;
&lt;p&gt;在大型 IT 架構中，多個系統（如 ERP、CRM、電商網站）往往會共用相同的數據。若沒有 SSOT，當客戶修改地址時，若只更新了 CRM 而沒更新 ERP，就會導致出貨錯誤。實施 SSOT 後，所有系統都會向「客戶主資料庫」看齊。&lt;/p&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;2-降低數據冗餘與維護成本&quot;&gt;2. 降低數據冗餘與維護成本&lt;/h3&gt;
&lt;p&gt;重複的數據存儲不僅浪費空間，更增加了維護難度。透過 SSOT，開發人員不需要編寫複雜的邏輯來比對多個資料庫之間的差異，簡化了系統整合的複雜度。&lt;/p&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;3-提升決策與分析的準確性&quot;&gt;3. 提升決策與分析的準確性&lt;/h3&gt;
&lt;p&gt;對於商業智慧（BI）與大數據分析而言，SSOT 是基礎。如果財務部門和銷售部門提供的營收報表數據對不起來（可能因為計算邏輯或數據來源不同），管理層將難以做出正確判斷。SSOT 確保了「數據來源一致」。&lt;/p&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;4-加速開發與自動化&quot;&gt;4. 加速開發與自動化&lt;/h3&gt;
&lt;p&gt;在 DevOps 或現代軟體開發中，SSOT 的概念也被應用於：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Infrastructure as Code (IaC):&lt;/strong&gt; 例如 Git 就是基礎設施配置的 SSOT。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;API 驅動架構:&lt;/strong&gt; 透過統一的 API 接口存取權威數據，避免直接對原始資料庫進行多點讀寫。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;實施-ssot-的挑戰&quot;&gt;實施 SSOT 的挑戰&lt;/h2&gt;
&lt;p&gt;雖然 SSOT 理想上很完美，但在實際運作中常遇到以下困難：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;跨部門溝通:&lt;/strong&gt; 定義「誰才是權威來源」往往涉及組織權責分配。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;系統效能:&lt;/strong&gt; 若所有系統都即時請求同一個數據源，可能造成效能瓶頸（通常透過緩存同步機制解決）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;舊系統轉型:&lt;/strong&gt; 整合缺乏標準接口的舊有系統（Legacy Systems）需要極大成本。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/5230391428804839554/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/09/ssot-single-source-of-truth.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/5230391428804839554'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/5230391428804839554'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/09/ssot-single-source-of-truth.html' title='SSOT Single Source of Truth'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-8136395257886646659</id><published>2026-08-31T08:30:59.277+08:00</published><updated>2026-08-31T08:30:59.278+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="linux"/><title type='text'>tc (traffic control) in linux</title><content type='html'>&lt;p&gt;tc（&lt;strong&gt;traffic control&lt;/strong&gt;）是 Linux 裡用來做網路流量控制（QoS, Quality of Service）的工具。可針對網路卡，進行限速（rate limiting）、排程（traffic scheduling）、優先權控制（priority）、模擬網路狀況（延遲、丟包、抖動）、封包分類與整形（shaping）的網路封包控制。主要用途是用來模擬網際網路的封包狀況。&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;流量控制方式&quot;&gt;流量控制方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;shaping&lt;/p&gt;
&lt;p&gt;當流量被shaping限制後，它的傳輸速率就被控制在某個值以下。限制值可以遠小於有效頻寬，這樣可以平滑突發的資料流量，使網路更爲穩定。shaping（限制）只適用於向外的流量。shaping 只能用在 egress 對外的資料輸出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;scheduling&lt;/p&gt;
&lt;p&gt;通過調度封包的傳輸，可以在頻寬範圍內，按照優先等級分配頻寬。scheduling 只能用在 egress 對外的資料輸出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;policing&lt;/p&gt;
&lt;p&gt;shaping用於處理 egress 對外的流量，而 policing 用於處理 ingress 接收到的流量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;dropping&lt;/p&gt;
&lt;p&gt;如果流量超過某個設定的頻寬，就丟棄資料，可用在 egress 及 ingress。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;主要元件&quot;&gt;主要元件&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;qdisc (Queueing Discipline)&lt;/p&gt;
&lt;p&gt;佇列規則，決定封包排隊的送出規則。當kernel需要通過某個網絡port發送資料，它都需要按照爲這個port配置的qdisc排隊規則，把資料加入queue。然後kernel會儘可能地從qdisc裏面取出資料，交給網絡適配器驅動模塊。最簡單的qdisc是 pfifo，它不對進入的封包做任何的處理，採用先入先出的方式通過隊列。不過，它會暫存網絡接口短時間內無法處理的封包。&lt;/p&gt;
&lt;p&gt;常見的 qdisc：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;pfifo_fast&lt;/code&gt;（預設）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;htb&lt;/code&gt;（常用做限速）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;fq_codel&lt;/code&gt;（改善 bufferbloat）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;netem&lt;/code&gt;（模擬網路）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;class&lt;/p&gt;
&lt;p&gt;在 qdisc 底下建立不同流量分類，用來分配不同頻寬。不同的類別中可以包含更深入的QDisc(排隊規則)，通過這些細分的QDisc還可以爲進入的隊列的封包排隊。通過設置各種類別封包的離隊次序，QDisc可以設定網路數據流量的優先等級。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;filter, policy 過濾器&lt;/p&gt;
&lt;p&gt;用來把封包分類到不同 class，決定它們按照何種QDisc進入隊列。分類的方法可以有多種，使用fileter(過濾器)就是其中之一。使用filter(過濾器)分類時，kernel會呼叫附屬於這個 class 的所有過濾器，直到回傳一個判決。如果沒有判決返回，就作進一步的處理，而處理方式和QDISC有關。filter(過濾器)是放在QDisc內部。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;指令&quot;&gt;指令&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tc [OPTIONS] OBJECT COMMAND&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常見 OBJECT：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;qdisc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;class&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;filter&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;指令內可使用的資料單位&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;頻寬或流量單位&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block&quot;&gt;kbps
mbps
kbit
mbit
bps&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;資料單位&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block&quot;&gt;kb
mb
mbit
kbit
b&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;時間&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block&quot;&gt;s/sec/secs
ms/msec/msecs
us/usec/usecs&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;options&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;-s, -stats, -statistics: output more statistics about packet usage.
-d, -details: output more detailed information about rates and cell sizes.
-r, -raw: output raw hex values for handles.
-p, -pretty: decode filter offset and mask values to equivalent filter commands based on TCP/IP.
-iec: print rates in IEC units (ie. 1K = 1024).&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;examples&quot;&gt;Examples&lt;/h2&gt;
&lt;p&gt;tc 通常是控制 egress 的流量&lt;/p&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;查看設定&quot;&gt;查看設定&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tc -s qdisc show dev eth0
tc class show dev eth0
tc filter show dev eth0&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# tc -s qdisc show dev enp1s0
qdisc mq 0: root
 Sent 220264 bytes 431 pkt (dropped 0, overlimits 0 requeues 2)
 backlog 0b 0p requeues 2
qdisc fq_codel 0: parent :4 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64
 Sent 20103 bytes 107 pkt (dropped 0, overlimits 0 requeues 0)
 backlog 0b 0p requeues 0
  maxpacket 0 drop_overlimit 0 new_flow_count 0 ecn_mark 0
  new_flows_len 0 old_flows_len 0
qdisc fq_codel 0: parent :3 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64
 Sent 43974 bytes 148 pkt (dropped 0, overlimits 0 requeues 0)
 backlog 0b 0p requeues 0
  maxpacket 0 drop_overlimit 0 new_flow_count 0 ecn_mark 0
  new_flows_len 0 old_flows_len 0
qdisc fq_codel 0: parent :2 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64
 Sent 146267 bytes 124 pkt (dropped 0, overlimits 0 requeues 2)
 backlog 0b 0p requeues 2
  maxpacket 8925 drop_overlimit 0 new_flow_count 1 ecn_mark 0
  new_flows_len 0 old_flows_len 0
qdisc fq_codel 0: parent :1 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64
 Sent 9920 bytes 52 pkt (dropped 0, overlimits 0 requeues 0)
 backlog 0b 0p requeues 0
  maxpacket 0 drop_overlimit 0 new_flow_count 0 ecn_mark 0
  new_flows_len 0 old_flows_len 0
# tc -d qdisc show dev enp1s0
qdisc mq 0: root
qdisc fq_codel 0: parent :4 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64
qdisc fq_codel 0: parent :3 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64
qdisc fq_codel 0: parent :2 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64
qdisc fq_codel 0: parent :1 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;qdisc mq 0: root&lt;/p&gt;
&lt;p&gt;根節點 mq : multi-queue&lt;/p&gt;
&lt;p&gt;代表你的硬體網路卡有多個傳輸佇列（在這個例子中是 4 個，分別對應 &lt;code&gt;parent :1&lt;/code&gt; 到 &lt;code&gt;:4&lt;/code&gt;）。通常發生在多核心 CPU 的伺服器上，系統會將網路流量分散到不同的 CPU 核心處理，以提升效能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;fq_codel (Fair Queuing Controlled Delay)&lt;/p&gt;
&lt;p&gt;這是目前 Linux 系統非常推薦的排隊規則，它由兩個部分組成&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FQ (Fair Queuing)&lt;/strong&gt;：確保每個「連線」（Flow）都能公平分配頻寬。例如，即使有人在下載大檔案，你的網頁瀏覽或視訊通話依然能順暢通過。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CoDel (Controlled Delay)&lt;/strong&gt;：這是一個智慧演算法，當它發現緩衝區（Buffer）塞車太久時，會主動丟棄封包或標記 ECN，強制降低延遲，解決「緩衝區膨脹」問題。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;以其中一個 &lt;code&gt;fq_codel&lt;/code&gt; 為例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;limit 10240p&lt;/strong&gt;: 佇列中最多可以存放 10,240 個封包。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;flows 1024&lt;/strong&gt;: 它最多可以同時追蹤並隔離 1024 個不同的連線。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;target 5ms / interval 100ms&lt;/strong&gt;: 這是 CoDel 的核心參數。它會嘗試將延遲控制在 &lt;strong&gt;5ms&lt;/strong&gt; 以內；如果延遲超過這個數值持續 &lt;strong&gt;100ms&lt;/strong&gt;，它就會開始干預流量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ecn&lt;/strong&gt;: 支援顯式擁塞通知。當網路塞車時，它會通知發送端減速，而不是直接丟棄封包。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;-s 統計&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Sent 146267 bytes 124 pkt (dropped 0, overlimits 0 requeues 2)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sent&lt;/strong&gt;: 該佇列目前傳送的資料量與封包數。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;dropped 0&lt;/strong&gt;: &lt;strong&gt;這是一個好現象。&lt;/strong&gt; 代表目前沒有封包因為塞車而被強制丟棄。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;requeues 2&lt;/strong&gt;: 封包重新排隊的次數。通常發生在硬體驅動程式暫時無法處理封包時（例如網路卡發送視窗滿了），這在正常運作下是偶爾會發生的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;new_flow_count 1&lt;/strong&gt;: 代表自從統計開始以來，有一個「新連線」被偵測到。&lt;code&gt;fq_codel&lt;/code&gt; 會優先處理新連線（互動式流量），以保證反應速度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;刪除設定&quot;&gt;刪除設定&lt;/h3&gt;
&lt;p&gt;清除所有設定&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tc qdisc del dev eth0 root&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;備份還原完整設定&quot;&gt;備份/還原完整設定&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# 備份
tc -dump qdisc show dev eth0 &amp;gt; tc-backup.txt
tc -dump class show dev eth0 &amp;gt;&amp;gt; tc-backup.txt
tc -dump filter show dev eth0 &amp;gt;&amp;gt; tc-backup.txt

# 還原前先清空
tc qdisc del dev eth0 root
tc qdisc del dev eth0 ingress
tc -batch tc-backup.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整備份所有網卡&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tc -dump qdisc show &amp;gt; tc-all.txt
tc -dump class show &amp;gt;&amp;gt; tc-all.txt
tc -dump filter show &amp;gt;&amp;gt; tc-all.txt

# 還原
tc -batch tc-all.txt&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;限制整個網卡頻寬&quot;&gt;限制整個網卡頻寬&lt;/h3&gt;
&lt;p&gt;限制 eth0 最大 1Mbps&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tc qdisc add dev eth0 root tbf rate 1mbit burst 32kbit latency 400ms&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;tbf = Token Bucket Filter（簡單限速）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;rate = 頻寬&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;burst = 突發流量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;latency = 最大延遲&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;使用-htb-做分流&quot;&gt;使用 HTB 做分流&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# 建立 root qdisc
tc qdisc add dev eth0 root handle 1: htb default 30

# 建立 class 做頻寬分配
tc class add dev eth0 parent 1: classid 1:1 htb rate 10mbit
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 5mbit
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 2mbit&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;1: (root)
 └── 1:1 (10mbit)
      ├── 1:10 (5mbit)
      └── 1:20 (2mbit)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;依-ip-分流&quot;&gt;依 IP 分流&lt;/h3&gt;
&lt;p&gt;ex&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block&quot;&gt;192.168.1.100 → 5Mbps
其他 → 2Mbps&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \
match ip dst 192.168.1.100 flowid 1:10&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;模擬網路延遲&quot;&gt;模擬網路延遲&lt;/h3&gt;
&lt;p&gt;增加 100ms 延遲&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tc qdisc add dev eth0 root netem delay 100ms&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;模擬丟包&quot;&gt;模擬丟包&lt;/h3&gt;
&lt;p&gt;10% 封包丟失&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tc qdisc add dev eth0 root netem loss 10%&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;延遲--抖動--丟包&quot;&gt;延遲 + 抖動 + 丟包&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tc qdisc add dev eth0 root netem delay 100ms 20ms loss 5%&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;100ms&lt;/code&gt; = 基本延遲&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;20ms&lt;/code&gt; = 抖動（jitter）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;loss 5%&lt;/code&gt; = 丟包&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;控制-ingress&quot;&gt;控制 ingress&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tc qdisc add dev eth0 handle ffff: ingress&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;ingress-shaping-ifb&quot;&gt;ingress shaping ifb&lt;/h3&gt;
&lt;p&gt;tc 原生只能控制 egress（送出流量） ，要控制 ingress（進來的流量），就要「轉送」到 ifb 再做 shaping&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;eth0 (ingress)
   ↓ redirect
ifb0 (egress)
   ↓ tc 控制&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;限制下載速度（ingress-10mbps）&quot;&gt;限制下載速度（ingress 10Mbps）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# 載入 ifb 模組
modprobe ifb

# 指定數量
modprobe ifb numifbs=1

# 啟用 ifb0
ip link add ifb0 type ifb
ip link set dev ifb0 up

# 在 eth0 加 ingress qdisc
tc qdisc add dev eth0 handle ffff: ingress

# 把 ingress 流量導到 ifb0
# mirred = mirror/redirect
tc filter add dev eth0 parent ffff: protocol ip u32 \
match u32 0 0 action mirred egress redirect dev ifb0

# 在 ifb0 上做限速
tc qdisc add dev ifb0 root tbf rate 10mbit burst 32k latency 400ms

# show
tc -s qdisc show dev ifb0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;clear&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tc qdisc del dev eth0 ingress
tc qdisc del dev ifb0 root
ip link delete ifb0&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;不同-ip-不同下載速度&quot;&gt;不同 IP 不同下載速度&lt;/h3&gt;
&lt;p&gt;ex&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block&quot;&gt;192.168.1.100 → 5 Mbps
其他 → 2 Mbps&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 HTB&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tc qdisc add dev ifb0 root handle 1: htb default 20

tc class add dev ifb0 parent 1: classid 1:1 htb rate 10mbit

tc class add dev ifb0 parent 1:1 classid 1:10 htb rate 5mbit
tc class add dev ifb0 parent 1:1 classid 1:20 htb rate 2mbit

# filter 分流
# ingress → ifb 後.  source IP 才是外部來源 IP
tc filter add dev ifb0 protocol ip parent 1: prio 1 u32 \
match ip src 192.168.1.100 flowid 1:10&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;note: ingress 只能有一個 qdisc&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://puremonkey2010.blogspot.com/2015/01/linux-tc-traffic-control.html&quot;&gt;程式扎記: [Linux 文章收集] TC 入門 (traffic control)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://hackmd.io/@st9540808/Bkw60WEfU&quot;&gt;TC (Traffic Control) - HackMD&lt;/a&gt;&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/8136395257886646659/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/08/tc-traffic-control-in-linux.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/8136395257886646659'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/8136395257886646659'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/08/tc-traffic-control-in-linux.html' title='tc (traffic control) in linux'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-1040883849001907619</id><published>2026-08-24T08:26:26.771+08:00</published><updated>2026-08-24T08:26:26.771+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>Java Multimap</title><content type='html'>&lt;p&gt;一般基本的 Map 是唯一的 Key，搭配一個 Value 的資料結構，實務上，有時候會遇到，一個 Key，搭配對應的 Value，在 Value 是有多種可能的，例如在公司部門分類下，每個部門都有各自的員工。&lt;/p&gt;
&lt;p&gt;這種資料結構，用標準 JDK 提供的 Map 搭配 ArrayList 就可以完成，但還有其他幾個封裝比較好的函式庫，有提供這種資料結構的類別實作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Apache Commons Collecitons 的 MultiValuedMap&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Guava 的 Multimap&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Spring Framework 的 MultiValueMap&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果要測試這幾種實作方式，要先引用函式庫&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-xml&quot;&gt;    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.google.guava&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;guava&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;33.5.0-jre&amp;lt;/version&amp;gt;
            &amp;lt;scope&amp;gt;compile&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-core&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;7.0.6&amp;lt;/version&amp;gt;
            &amp;lt;scope&amp;gt;compile&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.apache.commons&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;commons-collections4&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;4.5.0&amp;lt;/version&amp;gt;
            &amp;lt;scope&amp;gt;compile&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 實例&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import com.google.common.collect.Multimap;
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.util.*;

public class ArrayListMultimap {
    public void main(String[] args) {
        ArrayListMultimap.jdk();
        ArrayListMultimap.apacheCommons();
        ArrayListMultimap.guava();
        ArrayListMultimap.spring();
    }

    public static void addEmployee(Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; map, String department, String employee) {

        if (!map.containsKey(department)) {
            map.put(department, new ArrayList&amp;lt;&amp;gt;());
        }

        map.get(department).add(employee);
    }

    public static void jdk() {

        Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; departmentEmployees = new HashMap&amp;lt;&amp;gt;();

        addEmployee(departmentEmployees, &quot;RD&quot;, &quot;Thomas&quot;);
        addEmployee(departmentEmployees, &quot;RD&quot;, &quot;James&quot;);
        addEmployee(departmentEmployees, &quot;MA&quot;, &quot;Emma&quot;);
        addEmployee(departmentEmployees, &quot;MA&quot;, &quot;Dave&quot;);
        addEmployee(departmentEmployees, &quot;HR&quot;, &quot;Carol&quot;);

        System.out.println(&quot;=== JDK HashMap + List&amp;lt;String&amp;gt; ===&quot;);
        System.out.println(departmentEmployees);
    }
    public static void apacheCommons() {
        MultiValuedMap&amp;lt;String, String&amp;gt; departmentEmployees = new ArrayListValuedHashMap&amp;lt;&amp;gt;();

        departmentEmployees.put(&quot;RD&quot;, &quot;Thomas&quot;);
        departmentEmployees.put(&quot;RD&quot;, &quot;James&quot;);
        departmentEmployees.put(&quot;MA&quot;, &quot;Emma&quot;);
        departmentEmployees.put(&quot;MA&quot;, &quot;Dave&quot;);
        departmentEmployees.put(&quot;HR&quot;, &quot;Carol&quot;);

        System.out.println(&quot;=== Apache Commons ArrayListValuedHashMap ===&quot;);
        System.out.println(departmentEmployees);
    }
    public static void guava() {
        List&amp;lt;String&amp;gt; rdlist = Arrays.asList(&quot;Thomas&quot;, &quot;James&quot;);
        List&amp;lt;String&amp;gt; malist = Arrays.asList(&quot;Emma&quot;, &quot;Dave&quot;);
        List&amp;lt;String&amp;gt; hrlist = Arrays.asList(&quot;Carol&quot;);

        Multimap&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; listMultimap = com.google.common.collect.ArrayListMultimap.create();
        listMultimap.put(&quot;RD&quot;, rdlist);
        listMultimap.put(&quot;MA&quot;, malist);
        listMultimap.put(&quot;HR&quot;, hrlist);

        System.out.println(&quot;=== guava ArrayListMultimap ===&quot;);
        System.out.println(listMultimap);
    }

    public static void spring() {

        MultiValueMap&amp;lt;String, String&amp;gt; departmentEmployees = new LinkedMultiValueMap&amp;lt;&amp;gt;();

        departmentEmployees.add(&quot;RD&quot;, &quot;Thomas&quot;);
        departmentEmployees.add(&quot;RD&quot;, &quot;James&quot;);
        departmentEmployees.add(&quot;MA&quot;, &quot;Emma&quot;);
        departmentEmployees.add(&quot;MA&quot;, &quot;Dave&quot;);
        departmentEmployees.add(&quot;HR&quot;, &quot;Carol&quot;);

        System.out.println(&quot;=== Spring LinkedMultiValueMap ===&quot;);
        System.out.println(departmentEmployees);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;=== JDK HashMap + List&amp;lt;String&amp;gt; ===
{RD=[Thomas, James], MA=[Emma, Dave], HR=[Carol]}
=== Apache Commons ArrayListValuedHashMap ===
{RD=[Thomas, James], MA=[Emma, Dave], HR=[Carol]}
=== guava ArrayListMultimap ===
{RD=[[Thomas, James]], MA=[[Emma, Dave]], HR=[[Carol]]}
=== Spring LinkedMultiValueMap ===
{RD=[Thomas, James], MA=[Emma, Dave], HR=[Carol]}&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/1040883849001907619/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/08/java-multimap.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/1040883849001907619'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/1040883849001907619'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/08/java-multimap.html' title='Java Multimap'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-1193441333056077266</id><published>2026-08-17T08:37:24.846+08:00</published><updated>2026-08-17T08:37:24.846+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="erlang"/><title type='text'>erlang 的 == 與 =:=</title><content type='html'>&lt;p&gt;erlang 的相等比較有兩種 == 與 =:= 。同樣都是相等，但比較的嚴格等級不同。&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;&quot;&gt;==&lt;/h2&gt;
&lt;p&gt;這是等值比較，如果比較的是整數跟浮點數，兩種不同的資料型別，在比較之前，會自動做型別轉換，然後再根據數值進行比較。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-erlang&quot;&gt;&amp;gt; 1 == 1.0.
true
&amp;gt; 1 == 1.
true&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;-1&quot;&gt;=:=&lt;/h2&gt;
&lt;p&gt;全等比較，除了數值要一樣，資料型別也必須要一樣。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-erlang&quot;&gt;&amp;gt; 1 =:= 1.0.
false
&amp;gt; 1 =:= 1.
true&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;pattern-matching&quot;&gt;pattern matching&lt;/h2&gt;
&lt;p&gt;erlang 的 pattern matching 行為比較接近 =:=&lt;/p&gt;
&lt;p&gt;假設有這樣的一個 function&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-erlang&quot;&gt;check_num(1) -&amp;gt; integer;
check_num(1.0) -&amp;gt; float.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在呼叫 &lt;code&gt;chech_num(1.0)&lt;/code&gt; 的時候，會進入第二個 clause，不會進入第一個。因為 1 跟 1.0 是不同的資料型別&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;-與-&quot;&gt;/= 與 =/=&lt;/h2&gt;
&lt;p&gt;不相等，也一樣有兩種寫法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;/=&lt;/code&gt;&lt;/strong&gt;：對應於 &lt;code&gt;==&lt;/code&gt;。若值在算術上不相等，則為 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;=/=&lt;/code&gt;&lt;/strong&gt;：對應於 &lt;code&gt;=:=&lt;/code&gt;。若值或型別任一不同，則為 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-erlang&quot;&gt;&amp;gt;  1 /= 1.0.
false
&amp;gt; 1 =/= 1.0.
true&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;term-ordering&quot;&gt;Term Ordering&lt;/h2&gt;
&lt;p&gt;在 erlang 的所有資料型別，都能夠直接比較大小，這是因為在處理集合的排序時，能夠得到一個固定的結果。資料型別的比較順序如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block&quot;&gt;number &amp;lt; atom &amp;lt; reference &amp;lt; fun &amp;lt; port &amp;lt; pid &amp;lt; tuple &amp;lt; map &amp;lt; list &amp;lt; bitstring&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sample&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-erlang&quot;&gt;&amp;gt; 999999 &amp;lt; apple.
true
&amp;gt; Mixed = [apple, 10, {test, 1}, [1,2,3], 5.5, &quot;hello&quot;].
[apple,10,{test,1},[1,2,3],5.5,&quot;hello&quot;]
&amp;gt; lists:sort(Mixed).
[5.5,10,apple,{test,1},[1,2,3],&quot;hello&quot;]
% 順序：數字(5.5, 10) &amp;lt; atom(apple) &amp;lt; tuple({test,1}) &amp;lt; list([1,2,3], &quot;hello&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;要確保資料比對正確，大部分的狀況都是要寫成 =:=，僅有少部分，遇到了整數跟浮點數必須混合比較時，才需要用到 ==。&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/1193441333056077266/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/08/erlang.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/1193441333056077266'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/1193441333056077266'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/08/erlang.html' title='erlang 的 == 與 =:='/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-2382487884505151034</id><published>2026-08-10T08:32:34.563+08:00</published><updated>2026-08-10T08:32:34.563+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="linux"/><title type='text'>RPi 設定使用 camera</title><content type='html'>&lt;h2 class=&quot;atx&quot; id=&quot;camera&quot;&gt;camera&lt;/h2&gt;
&lt;p&gt;更新套件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;sudo apt update
sudo apt install ffmpeg v4l-utils libcamera-apps -y&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用一般的 web camera&lt;/p&gt;
&lt;p&gt;用這個指令列出所有 devices，通常外接的 camera 會在 /dev/video0&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;v4l2-ctl --list-devices&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用這個指令查詢 video0 支援的 video 格式&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;v4l2-ctl --device=/dev/video0 --list-formats-ext&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果有 RPi 官方提供的 camera 模組。&lt;/p&gt;
&lt;p&gt;安裝時&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;找到 CSI 相機接口，將拉起連接埠的黑色或白色塑膠卡榫&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;有藍色膠帶的一面要朝向 HDMI/USB 埠，金屬接點面朝向 CSI 接口內部&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;插入排線後，將卡榫壓回固定&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;測試拍照&quot;&gt;測試拍照&lt;/h2&gt;
&lt;p&gt;如果是用 web camera，可直接用 ffmpeg 拍照。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;ffmpeg -f v4l2 -video_size 640x480 -i /dev/video0 -frames:v 1 test.jpg&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是 RPI camera 模組，用 ffmpeg 會產生 error，要改用 &lt;code&gt;rpi-cam-*&lt;/code&gt; 指令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;rpicam-jpeg -o test.jpg

# video
rpicam-vid -t 5000 -o test.h264&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;將-rpi-改為-hotspot-ap&quot;&gt;將 RPi 改為 HotSpot AP&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# 將 wifi 改為 hotspot AP
sudo nmcli device wifi hotspot ssid crpi4 password wifipassword ifname wlan0

# 設定可自動連線
sudo nmcli connection modify Hotspot connection.autoconnect yes

# 修改為 2.4G
sudo nmcli connection modify Hotspot 802-11-wireless.band bg

# 設定可從 wifi 轉發到 ethernet
echo &quot;net.ipv4.ip_forward=1&quot; | sudo tee -a /etc/sysctl.conf
sudo sysctl -p&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;音效卡&quot;&gt;音效卡&lt;/h2&gt;
&lt;p&gt;使用 UPTech 161 USB 音效卡測試&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;$ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 2109:3431 VIA Labs, Inc. Hub
Bus 001 Device 004: ID 0d8c:0014 C-Media Electronics, Inc. Audio Adapter (Unitek Y-247A)
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要出現 C-Media 的那一行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones]
  Subdevices: 8/8
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
  Subdevice #4: subdevice #4
  Subdevice #5: subdevice #5
  Subdevice #6: subdevice #6
  Subdevice #7: subdevice #7
card 1: vc4hdmi0 [vc4-hdmi-0], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 2: vc4hdmi1 [vc4-hdmi-1], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 3: Device [USB Audio Device], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有出現 card 3： USB Audio&lt;/p&gt;
&lt;p&gt;設定 USB 音效卡為預設輸出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;sudo raspi-config&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;進入 &lt;strong&gt;System Options&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Audio&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;選擇你的 &lt;strong&gt;USB Audio Device&lt;/strong&gt;，然後按下 Finish&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;調整音量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;alsamixer&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按下 &lt;strong&gt;F6&lt;/strong&gt; 鍵，選擇你的 &lt;strong&gt;USB Sound Device&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用方向鍵將 &lt;strong&gt;Speaker&lt;/strong&gt; 或 &lt;strong&gt;Mic&lt;/strong&gt; 的音量推高&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt; $ arecord -l
**** List of CAPTURE Hardware Devices ****
card 3: Device [USB Audio Device], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;card 3&lt;/code&gt; 和 &lt;code&gt;device 0&lt;/code&gt; 組合起來就是硬體代號 &lt;strong&gt;&lt;code&gt;hw:3,0&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;讓系統預設就抓這張卡，我們建立一個 ALSA 設定檔&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;sudo nano /etc/asound.conf

defaults.ctl.card 3
defaults.pcm.card 3
defaults.timer.card 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;測試&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;$ aplay -D hw:3,0 /usr/share/sounds/alsa/Front_Center.wav
Playing WAVE &#39;/usr/share/sounds/alsa/Front_Center.wav&#39; : Signed 16 bit Little Endian, Rate 48000 Hz, Mono
aplay: set_params:1398: Channels count non available&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因為這張 USB 音效卡（或是 ALSA 驅動）在處理 &lt;strong&gt;Mono（單聲道）&lt;/strong&gt; 檔案時，無法直接對應到硬體的 &lt;strong&gt;Stereo（雙聲道）&lt;/strong&gt; 輸出。&lt;/p&gt;
&lt;p&gt;改為以下指令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;$ aplay -D plughw:3,0 /usr/share/sounds/alsa/Front_Center.wav
Playing WAVE &#39;/usr/share/sounds/alsa/Front_Center.wav&#39; : Signed 16 bit Little Endian, Rate 48000 Hz, Mono&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;測試錄音 10秒&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;arecord -D plughw:3,0 -d 10 -f cd -vv test_mic.wav&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;設定為 0dB&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;amixer -c 3 sset Mic 0dB&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;錄影錄音&quot;&gt;錄影+錄音&lt;/h2&gt;
&lt;p&gt;透過 rpicam-vid 錄影後，轉發給 ffmpeg 增加錄音&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;-f segment: 自動分割&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;-segment_time 600: 每 600 秒 (10分鐘) 分割一次&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;-reset_timestamps 1: 每個片段的時間戳從 0 開始，避免播放器錯誤&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;-strftime 1: 允許檔名使用時間格式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;rpicam-vid -t 0 --inline --width 1920 --height 1080 --framerate 30 \
    --bitrate 8000000 --profile high \
    --saturation 1.4 --contrast 1.2 --sharpness 1.5 --awb auto \
    -o - 2&amp;gt;/dev/null | \
ffmpeg -y -r 30 -i pipe:0 \
    -f alsa -i plughw:3,0 \
    -c:v copy -c:a aac -ac 1 \
    -af &quot;volume=2.0&quot; \
    -f segment -segment_time 300 -segment_format mp4 \
    /home/charley/Videos/test_%04d.mp4 &amp;gt; /dev/null 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;錄影 mp4 格式，有可能會因為中途中斷而打不開檔案。改使用 ts 格式。iMovie 可讀取 mp4 及 ts 格式&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;rpicam-vid -t 0 --inline --width 1920 --height 1080 --framerate 30 \
    --bitrate 8000000 --profile high \
    --saturation 1.4 --contrast 1.2 --sharpness 1.5 --awb auto \
    -o - 2&amp;gt;/dev/null | \
ffmpeg -y -r 30 -i pipe:0 \
    -f alsa -i plughw:3,0 \
    -c:v copy -c:a aac -ac 1 \
    -af &quot;volume=2.0&quot; \
    -f segment -segment_time 300 -segment_format mpegts \
    /home/charley/Videos/test_%04d.ts &amp;gt; /dev/null 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;調整畫質&quot;&gt;調整畫質&lt;/h2&gt;
&lt;p&gt;AWB 模式&lt;/p&gt;
&lt;p&gt;如果你覺得自動模式不準，可以嘗試適合的模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;--awb auto&lt;/code&gt;&lt;/strong&gt;: 預設自動。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;--awb indoor&lt;/code&gt;&lt;/strong&gt;: 適合一般室內燈光。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;--awb tungsten&lt;/code&gt;&lt;/strong&gt;: 適合黃光燈泡環境（這會強力補償藍色，消除黃感）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;--awb fluorescent&lt;/code&gt;&lt;/strong&gt;: 適合辦公室日光燈環境。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;或是使用 manual mode&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;--awbgains R,B&lt;/code&gt;&lt;/strong&gt;: 第一個數字是紅增益，第二個是藍增益。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;調低第一個數 (紅)&lt;/strong&gt; 或 &lt;strong&gt;調高第二個數 (藍)&lt;/strong&gt; 可以讓畫面變冷色調。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;rpicam-jpeg --width 1920 --height 1080 --awbgains 1.2,1.8 -o test_cool.jpg&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/2382487884505151034/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/08/rpi-camera.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/2382487884505151034'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/2382487884505151034'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/08/rpi-camera.html' title='RPi 設定使用 camera'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-5051052531341614965</id><published>2026-08-03T08:38:06.853+08:00</published><updated>2026-08-03T08:38:06.853+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="linux"/><title type='text'>pdf OCR</title><content type='html'>&lt;p&gt;當遇到 pdf 複製文字是無法辨識的內容時，這是 pdf 保護文字資料的方法，可透過轉成圖片，再做 OCR 解決。&lt;/p&gt;
&lt;p&gt;安裝軟體&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# ghostscript. pdf -&amp;gt; 圖片
dnf install ghostscript

# 圖片
dnf install ImageMagick

# Tesseract OCR（圖片 → 文字）
sudo dnf install tesseract
tesseract --list-langs
# 預設已安裝英文
#sudo dnf install tesseract-langpack-chi-sim  # 簡體中文，視需求
#sudo dnf install tesseract-langpack-chi-tra  # 繁體中文

# 合併 pdf
dnf install poppler-utils&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;轉成圖片&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;gs -dNOPAUSE -dBATCH -sDEVICE=png16m -r300 -sOutputFile=page_%03d.png input.pdf&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ocr 中文&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tesseract page_001.png output_001 -l chi-tra pdf&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ocr 英文&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tesseract page_001.png output_001 -l eng pdf&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;--psdm 參數，目前測試調整後， table 換行還是有問題&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--psm&lt;/code&gt; 常用模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;3&lt;/code&gt; → 全頁自動檢測文本&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;6&lt;/code&gt; → 單塊文本&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;11&lt;/code&gt; → 密集文字或多欄&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;tesseract page_001.png output_001 -l eng --psm 3 pdf&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多頁&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;for f in page_*.png; do
  base=$(basename $f .png)
  tesseract &quot;$f&quot; &quot;${base}_ocr&quot; -l eng pdf
done&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;合併&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;pdfunite page_*_ocr.pdf final_output.pdf&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;壓縮&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;gs -sDEVICE=pdfwrite -dPDFSETTINGS=/ebook -o compressed.pdf final_output.pdf&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/5051052531341614965/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/08/pdf-ocr.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/5051052531341614965'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/5051052531341614965'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/08/pdf-ocr.html' title='pdf OCR'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-7192103488505634139</id><published>2026-07-13T08:36:08.606+08:00</published><updated>2026-07-13T08:36:08.606+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="linux"/><title type='text'>rclone</title><content type='html'>&lt;h1 class=&quot;atx&quot; id=&quot;rclone&quot;&gt;rclone&lt;/h1&gt;
&lt;p&gt;rclone 等同雲端版 rsync，可以同步 / 備份資料到雲端，在不同雲端之間拷資料，把雲端 掛載成資料夾mount。支援 Google Drive、OneDrive、Dropbox、S3、Backblaze B2、FTP、SFTP、WebDAV。&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;ftp&quot;&gt;ftp&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;rclone config

NOTICE: Config file &quot;/root/.config/rclone/rclone.conf&quot; not found - using defaults
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;設定過程&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;n/s/q&amp;gt; n

Enter name for new remote.
name&amp;gt; ftp1

Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.
 1 / 1Fichier
   \ (fichier)
 2 / Akamai NetStorage
   \ (netstorage)
 3 / Alias for an existing remote
   \ (alias)
 4 / Amazon S3 Compliant Storage Providers including AWS, Alibaba, ArvanCloud, Ceph, ChinaMobile, Cloudflare, Cubbit, DigitalOcean, Dreamhost, Exaba, FileLu, FlashBlade, GCS, Hetzner, HuaweiOBS, IBMCOS, IDrive, Intercolo, IONOS, Leviia, Liara, Linode, LyveCloud, Magalu, Mega, Minio, Netease, Outscale, OVHcloud, Petabox, Qiniu, Rabata, RackCorp, Rclone, Scaleway, SeaweedFS, Selectel, Servercore, SpectraLogic, StackPath, Storj, Synology, TencentCOS, Wasabi, Zata, Other
   \ (s3)
 5 / Backblaze B2
   \ (b2)
 6 / Better checksums for other remotes
   \ (hasher)
 7 / Box
   \ (box)
 8 / Cache a remote
   \ (cache)
 9 / Citrix Sharefile
   \ (sharefile)
10 / Cloudinary
   \ (cloudinary)
11 / Combine several remotes into one
   \ (combine)
12 / Compress a remote
   \ (compress)
13 / DOI datasets
   \ (doi)
14 / Dropbox
   \ (dropbox)
15 / Encrypt/Decrypt a remote
   \ (crypt)
16 / Enterprise File Fabric
   \ (filefabric)
17 / FTP
   \ (ftp)
18 / FileLu Cloud Storage
   \ (filelu)
19 / Files.com
   \ (filescom)
20 / Gofile
   \ (gofile)
21 / Google Cloud Storage (this is not Google Drive)
   \ (google cloud storage)
22 / Google Drive
   \ (drive)
23 / Google Photos
   \ (google photos)
24 / HTTP
   \ (http)
25 / Hadoop distributed file system
   \ (hdfs)
26 / HiDrive
   \ (hidrive)
27 / ImageKit.io
   \ (imagekit)
28 / In memory object storage system.
   \ (memory)
29 / Internet Archive
   \ (internetarchive)
30 / Jottacloud
   \ (jottacloud)
31 / Koofr, Digi Storage and other Koofr-compatible storage providers
   \ (koofr)
32 / Linkbox
   \ (linkbox)
33 / Local Disk
   \ (local)
34 / Mail.ru Cloud
   \ (mailru)
35 / Mega
   \ (mega)
36 / Microsoft Azure Blob Storage
   \ (azureblob)
37 / Microsoft Azure Files
   \ (azurefiles)
38 / Microsoft OneDrive
   \ (onedrive)
39 / OpenDrive
   \ (opendrive)
40 / OpenStack Swift (Rackspace Cloud Files, Blomp Cloud Storage, Memset Memstore, OVH)
   \ (swift)
41 / Oracle Cloud Infrastructure Object Storage
   \ (oracleobjectstorage)
42 / Pcloud
   \ (pcloud)
43 / PikPak
   \ (pikpak)
44 / Pixeldrain Filesystem
   \ (pixeldrain)
45 / Proton Drive
   \ (protondrive)
46 / Put.io
   \ (putio)
47 / QingCloud Object Storage
   \ (qingstor)
48 / Quatrix by Maytech
   \ (quatrix)
49 / Read archives
   \ (archive)
50 / SMB / CIFS
   \ (smb)
51 / SSH/SFTP
   \ (sftp)
52 / Sia Decentralized Cloud
   \ (sia)
53 / Storj Decentralized Cloud Storage
   \ (storj)
54 / Sugarsync
   \ (sugarsync)
55 / Transparently chunk/split large files
   \ (chunker)
56 / Uloz.to
   \ (ulozto)
57 / Union merges the contents of several upstream fs
   \ (union)
58 / Uptobox
   \ (uptobox)
59 / WebDAV
   \ (webdav)
60 / Yandex Disk
   \ (yandex)
61 / Zoho
   \ (zoho)
62 / iCloud Drive
   \ (iclouddrive)
63 / premiumize.me
   \ (premiumizeme)
64 / seafile
   \ (seafile)
Storage&amp;gt; ftp

Option host.
FTP host to connect to.
E.g. &quot;ftp.example.com&quot;.
Enter a value.
host&amp;gt; 192.168.1.3

Option user.
FTP username.
Enter a value of type string. Press Enter for the default (root).
user&amp;gt; maxkit

Option port.
FTP port number.
Enter a signed integer. Press Enter for the default (21).
port&amp;gt; 21

Option pass.
FTP password.
Choose an alternative below. Press Enter for the default (n).
y) Yes, type in my own password
g) Generate random password
n) No, leave this optional password blank (default)
y/g/n&amp;gt; y
Enter the password:
password:
Confirm the password:
password:

Option tls.
Use Implicit FTPS (FTP over TLS).
When using implicit FTP over TLS the client connects using TLS
right from the start which breaks compatibility with
non-TLS-aware servers. This is usually served over port 990 rather
than port 21. Cannot be used in combination with explicit FTPS.
Enter a boolean value (true or false). Press Enter for the default (false).
tls&amp;gt; false

Option explicit_tls.
Use Explicit FTPS (FTP over TLS).
When using explicit FTP over TLS the client explicitly requests
security from the server in order to upgrade a plain text connection
to an encrypted one. Cannot be used in combination with implicit FTPS.
Enter a boolean value (true or false). Press Enter for the default (false).
explicit_tls&amp;gt;

Edit advanced config?
y) Yes
n) No (default)
y/n&amp;gt; y

Option concurrency.
Maximum number of FTP simultaneous connections, 0 for unlimited.
Note that setting this is very likely to cause deadlocks so it should
be used with care.
If you are doing a sync or copy then make sure concurrency is one more
than the sum of `--transfers` and `--checkers`.
If you use `--check-first` then it just needs to be one more than the
maximum of `--checkers` and `--transfers`.
So for `concurrency 3` you&#39;d use `--checkers 2 --transfers 2
--check-first` or `--checkers 1 --transfers 1`.
Enter a signed integer. Press Enter for the default (0).
concurrency&amp;gt; 3

Option no_check_certificate.
Do not verify the TLS certificate of the server.
Enter a boolean value (true or false). Press Enter for the default (false).
no_check_certificate&amp;gt;

Option disable_epsv.
Disable using EPSV even if server advertises support.
Enter a boolean value (true or false). Press Enter for the default (false).
disable_epsv&amp;gt;

Option disable_mlsd.
Disable using MLSD even if server advertises support.
Enter a boolean value (true or false). Press Enter for the default (false).
disable_mlsd&amp;gt;

Option disable_utf8.
Disable using UTF-8 even if server advertises support.
Enter a boolean value (true or false). Press Enter for the default (false).
disable_utf8&amp;gt;

Option writing_mdtm.
Use MDTM to set modification time (VsFtpd quirk)
Enter a boolean value (true or false). Press Enter for the default (false).
writing_mdtm&amp;gt;

Option force_list_hidden.
Use LIST -a to force listing of hidden files and folders. This will disable the use of MLSD.
Enter a boolean value (true or false). Press Enter for the default (false).
force_list_hidden&amp;gt;

Option idle_timeout.
Max time before closing idle connections.
If no connections have been returned to the connection pool in the time
given, rclone will empty the connection pool.
Set to 0 to keep connections indefinitely.
Enter a duration s,m,h,d,w,M,y. Press Enter for the default (1m0s).
idle_timeout&amp;gt;

Option close_timeout.
Maximum time to wait for a response to close.
Enter a duration s,m,h,d,w,M,y. Press Enter for the default (1m0s).
close_timeout&amp;gt;

Option tls_cache_size.
Size of TLS session cache for all control and data connections.
TLS cache allows to resume TLS sessions and reuse PSK between connections.
Increase if default size is not enough resulting in TLS resumption errors.
Enabled by default. Use 0 to disable.
Enter a signed integer. Press Enter for the default (32).
tls_cache_size&amp;gt;

Option disable_tls13.
Disable TLS 1.3 (workaround for FTP servers with buggy TLS)
Enter a boolean value (true or false). Press Enter for the default (false).
disable_tls13&amp;gt;

Option allow_insecure_tls_ciphers.
Allow insecure TLS ciphers
Setting this flag will allow the usage of the following TLS ciphers in addition to the secure defaults:
- TLS_RSA_WITH_AES_128_GCM_SHA256
Enter a boolean value (true or false). Press Enter for the default (false).
allow_insecure_tls_ciphers&amp;gt;

Option shut_timeout.
Maximum time to wait for data connection closing status.
Enter a duration s,m,h,d,w,M,y. Press Enter for the default (1m0s).
shut_timeout&amp;gt;

Option ask_password.
Allow asking for FTP password when needed.
If this is set and no password is supplied then rclone will ask for a password
Enter a boolean value (true or false). Press Enter for the default (false).
ask_password&amp;gt;

Option socks_proxy.
Socks 5 proxy host.

Supports the format user:pass@host:port, user@host:port, host:port.

Example:

    myUser:myPass@localhost:9005
Enter a value. Press Enter to leave empty.
socks_proxy&amp;gt;

Option http_proxy.
URL for HTTP CONNECT proxy
Set this to a URL for an HTTP proxy which supports the HTTP CONNECT verb.
Enter a value. Press Enter to leave empty.
http_proxy&amp;gt;

Option no_check_upload.
Don&#39;t check the upload is OK
Normally rclone will try to check the upload exists after it has
uploaded a file to make sure the size and modification time are as
expected.
This flag stops rclone doing these checks. This enables uploading to
folders which are write only.
You will likely need to use the --inplace flag also if uploading to
a write only folder.
Enter a boolean value (true or false). Press Enter for the default (false).
no_check_upload&amp;gt;

Option encoding.
The encoding for the backend.
See the [encoding section in the overview](/overview/#encoding) for more info.
Choose a number from below, or type in your own value of type Encoding.
Press Enter for the default (Slash,Del,Ctl,RightSpace,Dot).
 1 / ProFTPd can&#39;t handle &#39;*&#39; in file names
   \ (Asterisk,Ctl,Dot,Slash)
 2 / PureFTPd can&#39;t handle &#39;[]&#39; or &#39;*&#39; in file names
   \ (BackSlash,Ctl,Del,Dot,RightSpace,Slash,SquareBracket)
 3 / VsFTPd can&#39;t handle file names starting with dot
   \ (Ctl,LeftPeriod,Slash)
encoding&amp;gt;

Option description.
Description of the remote.
Enter a value. Press Enter to leave empty.
description&amp;gt;

Edit advanced config?
y) Yes
n) No (default)
y/n&amp;gt;

Configuration complete.
Options:
- type: ftp
- host: 192.168.1.3
- user: maxkit
- pass: *** ENCRYPTED ***
- concurrency: 3
Keep this &quot;ftp1&quot; remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d&amp;gt; y

Current remotes:

Name                 Type
====                 ====
ftp1                 ftp

e) Edit existing remote
n) New remote
d) Delete remote
r) Rename remote
c) Copy remote
s) Set configuration password
q) Quit config
e/n/d/r/c/s/q&amp;gt; q&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;測試，注意 ls 會遞迴進入所有目錄 list 所有檔案&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# list directory
rclone lsd ftp1:
# list file
rclone ls ftp1:&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到清單就代表連線 OK&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;mount 目錄&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# 建立目錄
mkdir -p /mnt/ftp1

# mount --daemon 會 fork 到前景，測試時可不需要用
rclone mount ftp1: /mnt/ftp1 \
  --vfs-cache-mode full \
  --vfs-cache-max-size 5G \
  --vfs-cache-max-age 24h \
  --allow-other \
  --daemon

ls /mnt/ftp1

# unmount
fusermount3 -u /mnt/ftp1
#rclone unmount /mnt/ftp1&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;systemd service&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vi /etc/systemd/system/rclone-ftp1.service&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;[Unit]
Description=Rclone FTP Mount
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/rclone mount ftp1: /mnt/ftp1 \
  --vfs-cache-mode full \
  --vfs-cache-max-size 5G \
  --vfs-cache-max-age 24h \
  --allow-other
ExecStop=/bin/fusermount3 -u /mnt/ftp1
Restart=always
User=root

[Install]
WantedBy=multi-user.target&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;啟用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;systemctl daemon-reload
systemctl enable --now rclone-ftp1&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;sftp&quot;&gt;sftp&lt;/h2&gt;
&lt;p&gt;設定&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;e/n/d/r/c/s/q&amp;gt; n

Enter name for new remote.
name&amp;gt; sftp1

Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.
 1 / 1Fichier
   \ (fichier)
 2 / Akamai NetStorage
   \ (netstorage)
 3 / Alias for an existing remote
   \ (alias)
 4 / Amazon S3 Compliant Storage Providers including AWS, Alibaba, ArvanCloud, Ceph, ChinaMobile, Cloudflare, Cubbit, DigitalOcean, Dreamhost, Exaba, FileLu, FlashBlade, GCS, Hetzner, HuaweiOBS, IBMCOS, IDrive, Intercolo, IONOS, Leviia, Liara, Linode, LyveCloud, Magalu, Mega, Minio, Netease, Outscale, OVHcloud, Petabox, Qiniu, Rabata, RackCorp, Rclone, Scaleway, SeaweedFS, Selectel, Servercore, SpectraLogic, StackPath, Storj, Synology, TencentCOS, Wasabi, Zata, Other
   \ (s3)
 5 / Backblaze B2
   \ (b2)
 6 / Better checksums for other remotes
   \ (hasher)
 7 / Box
   \ (box)
 8 / Cache a remote
   \ (cache)
 9 / Citrix Sharefile
   \ (sharefile)
10 / Cloudinary
   \ (cloudinary)
11 / Combine several remotes into one
   \ (combine)
12 / Compress a remote
   \ (compress)
13 / DOI datasets
   \ (doi)
14 / Dropbox
   \ (dropbox)
15 / Encrypt/Decrypt a remote
   \ (crypt)
16 / Enterprise File Fabric
   \ (filefabric)
17 / FTP
   \ (ftp)
18 / FileLu Cloud Storage
   \ (filelu)
19 / Files.com
   \ (filescom)
20 / Gofile
   \ (gofile)
21 / Google Cloud Storage (this is not Google Drive)
   \ (google cloud storage)
22 / Google Drive
   \ (drive)
23 / Google Photos
   \ (google photos)
24 / HTTP
   \ (http)
25 / Hadoop distributed file system
   \ (hdfs)
26 / HiDrive
   \ (hidrive)
27 / ImageKit.io
   \ (imagekit)
28 / In memory object storage system.
   \ (memory)
29 / Internet Archive
   \ (internetarchive)
30 / Jottacloud
   \ (jottacloud)
31 / Koofr, Digi Storage and other Koofr-compatible storage providers
   \ (koofr)
32 / Linkbox
   \ (linkbox)
33 / Local Disk
   \ (local)
34 / Mail.ru Cloud
   \ (mailru)
35 / Mega
   \ (mega)
36 / Microsoft Azure Blob Storage
   \ (azureblob)
37 / Microsoft Azure Files
   \ (azurefiles)
38 / Microsoft OneDrive
   \ (onedrive)
39 / OpenDrive
   \ (opendrive)
40 / OpenStack Swift (Rackspace Cloud Files, Blomp Cloud Storage, Memset Memstore, OVH)
   \ (swift)
41 / Oracle Cloud Infrastructure Object Storage
   \ (oracleobjectstorage)
42 / Pcloud
   \ (pcloud)
43 / PikPak
   \ (pikpak)
44 / Pixeldrain Filesystem
   \ (pixeldrain)
45 / Proton Drive
   \ (protondrive)
46 / Put.io
   \ (putio)
47 / QingCloud Object Storage
   \ (qingstor)
48 / Quatrix by Maytech
   \ (quatrix)
49 / Read archives
   \ (archive)
50 / SMB / CIFS
   \ (smb)
51 / SSH/SFTP
   \ (sftp)
52 / Sia Decentralized Cloud
   \ (sia)
53 / Storj Decentralized Cloud Storage
   \ (storj)
54 / Sugarsync
   \ (sugarsync)
55 / Transparently chunk/split large files
   \ (chunker)
56 / Uloz.to
   \ (ulozto)
57 / Union merges the contents of several upstream fs
   \ (union)
58 / Uptobox
   \ (uptobox)
59 / WebDAV
   \ (webdav)
60 / Yandex Disk
   \ (yandex)
61 / Zoho
   \ (zoho)
62 / iCloud Drive
   \ (iclouddrive)
63 / premiumize.me
   \ (premiumizeme)
64 / seafile
   \ (seafile)
Storage&amp;gt; sftp

Option host.
SSH host to connect to.
E.g. &quot;example.com&quot;.
Enter a value.
host&amp;gt; 192.168.1.3

Option user.
SSH username.
Enter a value of type string. Press Enter for the default (root).
user&amp;gt; maxkit

Option port.
SSH port number.
Enter a signed integer. Press Enter for the default (22).
port&amp;gt;

Option pass.
SSH password, leave blank to use ssh-agent.
Choose an alternative below. Press Enter for the default (n).
y) Yes, type in my own password
g) Generate random password
n) No, leave this optional password blank (default)
y/g/n&amp;gt; y
Enter the password:
password:
Confirm the password:
password:

Option key_pem.
Raw PEM-encoded private key.
Note that this should be on a single line with line endings replaced with &#39;\n&#39;, eg
    key_pem = -----BEGIN RSA PRIVATE KEY-----\nMaMbaIXtE\n0gAMbMbaSsd\nMbaass\n-----END RSA PRIVATE KEY-----
This will generate the single line correctly:
    awk &#39;{printf &quot;%s\\n&quot;, $0}&#39; &amp;lt; ~/.ssh/id_rsa
If specified, it will override the key_file parameter.
Enter a value. Press Enter to leave empty.
key_pem&amp;gt;

Option key_file.
Path to PEM-encoded private key file.
Leave blank or set key-use-agent to use ssh-agent.
Leading `~` will be expanded in the file name as will environment variables such as `${RCLONE_CONFIG_DIR}`.
Enter a value. Press Enter to leave empty.
key_file&amp;gt;

Option key_file_pass.
The passphrase to decrypt the PEM-encoded private key file.
Only PEM encrypted key files (old OpenSSH format) are supported. Encrypted keys
in the new OpenSSH format can&#39;t be used.
Choose an alternative below. Press Enter for the default (n).
y) Yes, type in my own password
g) Generate random password
n) No, leave this optional password blank (default)
y/g/n&amp;gt; y
Enter the password:
password:
Confirm the password:
password:

Option pubkey.
SSH public certificate for public certificate based authentication.
Set this if you have a signed certificate you want to use for authentication.
If specified will override pubkey_file.
Enter a value. Press Enter to leave empty.
pubkey&amp;gt;

Option pubkey_file.
Optional path to public key file.
Set this if you have a signed certificate you want to use for authentication.
Leading `~` will be expanded in the file name as will environment variables such as `${RCLONE_CONFIG_DIR}`.
Enter a value. Press Enter to leave empty.
pubkey_file&amp;gt;

Option key_use_agent.
When set forces the usage of the ssh-agent.
When key-file is also set, the &quot;.pub&quot; file of the specified key-file is read and only the associated key is
requested from the ssh-agent. This allows to avoid `Too many authentication failures for *username*` errors
when the ssh-agent contains many keys.
Enter a boolean value (true or false). Press Enter for the default (false).
key_use_agent&amp;gt;

Option use_insecure_cipher.
Enable the use of insecure ciphers and key exchange methods.
This enables the use of the following insecure ciphers and key exchange methods:
- aes128-cbc
- aes192-cbc
- aes256-cbc
- 3des-cbc
- diffie-hellman-group-exchange-sha256
- diffie-hellman-group-exchange-sha1
Those algorithms are insecure and may allow plaintext data to be recovered by an attacker.
This must be false if you use either ciphers or key_exchange advanced options.
Choose a number from below, or type in your own boolean value (true or false).
Press Enter for the default (false).
 1 / Use default Cipher list.
   \ (false)
 2 / Enables the use of the aes128-cbc cipher and diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1 key exchange.
   \ (true)
use_insecure_cipher&amp;gt;

Option disable_hashcheck.
Disable the execution of SSH commands to determine if remote file hashing is available.
Leave blank or set to false to enable hashing (recommended), set to true to disable hashing.
Enter a boolean value (true or false). Press Enter for the default (false).
disable_hashcheck&amp;gt;

Option ssh.
Path and arguments to external ssh binary.
Normally rclone will use its internal ssh library to connect to the
SFTP server. However it does not implement all possible ssh options so
it may be desirable to use an external ssh binary.
Rclone ignores all the internal config if you use this option and
expects you to configure the ssh binary with the user/host/port and
any other options you need.
**Important** The ssh command must log in without asking for a
password so needs to be configured with keys or certificates.
Rclone will run the command supplied either with the additional
arguments &quot;-s sftp&quot; to access the SFTP subsystem or with commands such
as &quot;md5sum /path/to/file&quot; appended to read checksums.
Any arguments with spaces in should be surrounded by &quot;double quotes&quot;.
An example setting might be:
    ssh -o ServerAliveInterval=20 user@example.com
Note that when using an external ssh binary rclone makes a new ssh
connection for every hash it calculates.
Enter a value of type SpaceSepList. Press Enter to leave empty.
ssh&amp;gt;

Edit advanced config?
y) Yes
n) No (default)
y/n&amp;gt; n

Configuration complete.
Options:
- type: sftp
- host: 192.168.1.3
- user: maxkit
- pass: *** ENCRYPTED ***
- key_file_pass: *** ENCRYPTED ***
Keep this &quot;sftp1&quot; remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d&amp;gt; y

Current remotes:

Name                 Type
====                 ====
ftp1                 ftp
sftp1                sftp

e) Edit existing remote
n) New remote
d) Delete remote
r) Rename remote
c) Copy remote
s) Set configuration password
q) Quit config
e/n/d/r/c/s/q&amp;gt; q&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;mount&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;mkdir -p /mnt/sftp1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;測試&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;rclone lsd sftp1:

rclone mount sftp1:/ /mnt/sftp1 \
  --allow-other \
  --vfs-cache-mode full \
  --buffer-size 64M \
  --daemon

ls -al /mnt/sftp1/
/bin/fusermount3 -u /mnt/sftp1&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;service&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vi /etc/systemd/system/rclone-sftp1.service&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;[Unit]
Description=Rclone SFTP Mount
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/rclone mount sftp1: /mnt/sftp1 \
  --vfs-cache-mode full \
  --buffer-size 64M \
  --allow-other
ExecStop=/bin/fusermount3 -u /mnt/sftp1
Restart=always
User=root

[Install]
WantedBy=multi-user.target&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;啟動&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;systemctl daemon-reload
systemctl enable --now rclone-sftp1&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;直接編輯設定檔&quot;&gt;直接編輯設定檔&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# pwd 手動用 rclone 自帶工具加密
rclone obscure &quot;yourpassword&quot;
# 把結果放到 pass = &amp;lt;OBSCURED_PASSWORD&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;設定檔&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block&quot;&gt;~/.config/rclone/rclone.conf

[ftp1]
type = ftp
host = 192.168.1.3
user = maxkit
pass = TwE1qxopK3QB87j3QOEn9-WH0HrZhhsC
concurrency = 3

[sftp1]
type = sftp
host = 192.168.1.3
user = maxkit
pass = EhKh5h2NNeX0W0FIej8M8xoP1o5rJeER
key_file_pass = TW5-tYJMrtLeF6ys1E2wIhM5Amm8PG5K
shell_type = unix
md5sum_command = md5sum
sha1sum_command = sha1sum&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;chmod 600 ~/.config/rclone/rclone.conf&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/7192103488505634139/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/07/rclone.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/7192103488505634139'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/7192103488505634139'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/07/rclone.html' title='rclone'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-6015275589961326683</id><published>2026-07-06T08:29:21.005+08:00</published><updated>2026-07-06T08:29:21.006+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>Spring AOP</title><content type='html'>
&lt;h2 class=&quot;atx&quot; id=&quot;aop-概念&quot;&gt;AOP 概念&lt;/h2&gt;
&lt;p&gt;Aspect-Oriented-Programming 就是在 java business service method 上加工，製造 method 呼叫前後的切面斷點，用以提供一些通用的商務邏輯，最常見的就是 business method 加上 database transaction 的機制。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.books.com.tw/products/CN11962188&quot;&gt;博客來-深入淺出Spring Boot 3.x&lt;/a&gt; 裡面有一個部分，嘗試用程式的方式，透過 spring 使用的 cglib 套件實作 AOP 的概念。&lt;/p&gt;
&lt;p&gt;spring boot 有兩種 proxy bean 的方法：JDK 或 CGLIB，預設是用 cglib，以下是用 cglib 的 Enhancer 產生動態 proxy bean。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;產生 service 跟 impl&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HelloService.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.interceptor.service;

public interface HelloService {
    public void sayHello(String name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HelloServiceImpl.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.interceptor.service.impl;

import com.test.interceptor.service.HelloService;

public class HelloServiceImpl implements HelloService {

    @Override
    public void sayHello(String name) {
        if (name == null || &quot;&quot;.equals(name.trim())) {
            throw new RuntimeException(&quot;parameter is null!!&quot;);
        }
        System.out.println(&quot;hello &quot; + name);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;interceptor&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Interceptor.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.interceptor.interceptor;

import com.test.interceptor.Invocation;

public interface Interceptor {
    // before method invoke
    public void before();

    // after method invoke
    public void after();

    /**
     * wrap method to add around actions
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    public Object around(Invocation invocation) throws Throwable;

    // after return if no exception
    public void afterReturning();

    // after return if throws exception
    public void afterThrowing();

    // use around to wrap method
    public default boolean useAround() {
        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;InterceptorImpl.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.interceptor.interceptor;

import com.test.interceptor.Invocation;

public class InterceptorImpl implements Interceptor {

    @Override
    public void before() {
        System.out.println(&quot;before ......&quot;);
    }

    @Override
    public boolean useAround() {
        return true;
    }

    @Override
    public void after() {
        System.out.println(&quot;after ......&quot;);
    }

    @Override
    public Object around(Invocation invocation) throws Throwable {
        System.out.println(&quot;around before ......&quot;);
        Object obj = invocation.proceed();
        System.out.println(&quot;around after ......&quot;);
        return obj;
    }

    @Override
    public void afterReturning() {
        System.out.println(&quot;afterReturning......&quot;);
    }

    @Override
    public void afterThrowing() {
        System.out.println(&quot;afterThrowing......&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;透過 Invocation 製作這個流程&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;呼叫 interceptor 的 before()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;呼叫 target object method&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;當 method throw exception，呼叫 afterThrowing()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;當 method 正常執行完成，呼叫 afterReturning()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;呼叫 after()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;如果 interceptor 的 useAround() 回傳 true，會用 around() 取代上面的流程&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Invocation.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.interceptor;

import com.test.interceptor.interceptor.Interceptor;
import lombok.Data;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

@Data
public class Invocation {
    private Object[] params;
    private Method method;
    private Object target;
    private Interceptor interceptor;

    public Invocation(Object target, Method method, Object[] params, Interceptor interceptor) {
        this.target = target;
        this.method = method;
        this.params = params;
        this.interceptor = interceptor;
    }

    // reflection
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        Object retObj = null; // return object
        boolean exceptionFlag = false; // exception flag
        // invoke before()
        this.interceptor.before();
        try {
            // use reflection to call method
            retObj = method.invoke(target, params);
        } catch (Exception ex) {
            // exception flag
            exceptionFlag = true;
        }
        if (exceptionFlag) {
            this.interceptor.afterThrowing();
        } else {
            this.interceptor.afterReturning();
        }
        this.interceptor.after();
        return retObj;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;將 target object 跟 interceptor 合併為流程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ProxyBean.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.interceptor;

import com.test.interceptor.interceptor.Interceptor;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class ProxyBean implements MethodInterceptor {
    private Interceptor interceptor = null;
    private Object target = null;

    /**
     * create proxy bean
     * @param target
     * @param interceptor
     * @return
     */
    public static Object getProxy(Object target, Interceptor interceptor) {
        var proxyBean = new ProxyBean();
        // create cglib Enhancer
        var enhancer = new Enhancer();
        // set proxy bean class
        enhancer.setSuperclass(target.getClass());
        // set proxy bean interfaces
        enhancer.setInterfaces(target.getClass().getInterfaces());
        // target object
        proxyBean.target = target;
        // interceptor
        proxyBean.interceptor = interceptor;
        // call proxyBean intercept()
        enhancer.setCallback(proxyBean);
        // create proxy bean
        var proxy = enhancer.create();
        return proxy;
    }

    /**
     * @param proxy  proxy bean
     * @param method interceptor method
     * @param args   method args
     * @param mproxy method proxy
     * @return return object
     */
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy mproxy) throws Throwable {
        // invoke proxy bean
        var invocation = new Invocation(this.target, method, args, this.interceptor);
        Object result = null;
        if (this.interceptor.useAround()) { // enable around
            result = this.interceptor.around(invocation);
        } else {
            result = invocation.proceed();
        }
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;測試&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;InterceptorTest.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.interceptor;

import com.test.interceptor.interceptor.InterceptorImpl;
import com.test.interceptor.service.HelloService;
import com.test.interceptor.service.impl.HelloServiceImpl;

public class InterceptorTest {

    public static void main(String[] args) {
        testProxy();
    }

    public static void testProxy() {
        // target object
        var helloService = new HelloServiceImpl();
        // create proxy bean &amp;amp; interceptor
        var proxy = (HelloService) ProxyBean.getProxy(helloService, new InterceptorImpl());
        // call proxy bean method
        proxy.sayHello(&quot;Hello World&quot;);
        System.out.println(&quot;###############Testing Exception###############&quot;);
        proxy.sayHello(null);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;執行結果&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;around before ......
before ......
hello Hello World
afterReturning......
after ......
around after ......
###############Testing Exception###############
around before ......
before ......
afterThrowing......
after ......
around after ......&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;aop-術語&quot;&gt;AOP 術語&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;join point 連接點&lt;/p&gt;
&lt;p&gt;連接點就是告訴 AOP 在哪裡需要做 AOP，因為 AOP 只支援 method，被攔截的一定只能是 method&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;point cut 切點&lt;/p&gt;
&lt;p&gt;有時候需要啟動 AOP 的不是單一個 method，而是多個類別的不同 method。這邊可透過 regular expressoin 與指示器的規則定義 point cut，讓 AOP 根據定義匹配多個 methods。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;advice 通知&lt;/p&gt;
&lt;p&gt;分為 before advice, after advice, around advice, afterReturning advice, afterThrowing advice&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;target 目標對象&lt;/p&gt;
&lt;p&gt;代理的物件對象，例如上面例子的 HelloServiceImpl&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;introduction 引用&lt;/p&gt;
&lt;p&gt;引用新的類別及 method，可增強現有bean的功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;weaving&lt;/p&gt;
&lt;p&gt;透過動態代理的技術，為目標物件生成代理物件。利用切點 join cut 的定義匹配的 join point 決定各類 advice 通知加入流程的過程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;aspect 切面&lt;/p&gt;
&lt;p&gt;一個類別，透過它定義 AOP 的 point cut, advice&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-mermaid&quot;&gt;flowchart TB
    a1[&quot;target: HelloServiceImpl.sayHello()&quot;]--&amp;gt;b2
    subgraph one [service]
    a1--&amp;gt;a2[&quot;cglib: weaving InterceptorImpl&quot;]
    end
    subgraph two [InterceptorImpl, around advice]
    b1[&quot;before advice&quot;]--&amp;gt;b2
    b2[&quot;method reflection&quot;]--&amp;gt;b3{exception?}
    b3--yes--&amp;gt;b4
    b3--no--&amp;gt;b5
    b4[&quot;afterThrowing advice&quot;]--&amp;gt;b6[&quot;after advice&quot;]
    b5[&quot;afterReturning advice&quot;]--&amp;gt;b6
    end&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;開發&quot;&gt;開發&lt;/h2&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-xml&quot;&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-aop&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;確認-target&quot;&gt;確認 target&lt;/h3&gt;
&lt;p&gt;假設有一個 UserService, UserServiceImpl，裡面有 printUser method，printUser 就是 target&lt;/p&gt;
&lt;p&gt;UserService.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.aspect.service;

import com.test.aspect.pojo.User;

public interface UserService {
    public void printUser(User user);

    public void multiAspects();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UserServiceImpl.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.aspect.service.impl;

import com.test.aspect.service.UserService;
import com.test.aspect.pojo.User;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    @Override
    public void printUser(User user) {
        if (user == null) {
            throw new RuntimeException(&quot;user is null!&quot;);
        }
        System.out.println(&quot;user=&quot; + user);
    }

    @Override
    public void multiAspects() {
        System.out.println(&quot;testing multiAspects&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;開發切面-aspect&quot;&gt;開發切面 @Aspect&lt;/h3&gt;
&lt;p&gt;內容主要是各種 Apsect advice&lt;/p&gt;
&lt;p&gt;@Before, @After, @AfterReturning, @AfterThrowing 分別用一個相同的 regular expression 定義哪個地方要啟用 AOP，匹配 target 的 join point 連結點(method)&lt;/p&gt;
&lt;p&gt;MyAspectTemp.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.aspect;

import com.test.aspect.pojo.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;

// 宣告為 Aspect，要加上 Component 註冊到 IoC
@Aspect
@Component
public class MyAspectTemp {
    // 以 regular expression，指定 join point (包含 class, method)
    // execution  代表在執行時，要攔截匹配的 regluar expression
    // * 代表不在意 method 回傳任何資料型別
    // com.test.aspect.service.impl.UserServiceImpl  指定 target object
    // printUser  指定 method
    // (..) 代表匹配任意參數
    private static final String aopExp = &quot;execution(* &quot; + &quot;com.test.aspect.service.impl.UserServiceImpl.printUser(..))&quot;;

    @Before(aopExp)
    public void before() {
        System.out.println(&quot;before ......&quot;);
    }

    @Before(aopExp)
    public void beforeParam(JoinPoint jp, User user) {
        System.out.println(&quot;beforeParam ......&quot;);
    }

    @After(aopExp)
    public void after() {
        System.out.println(&quot;after ......&quot;);
    }

    @AfterReturning(aopExp)
    public void afterReturning() {
        System.out.println(&quot;afterReturning ......&quot;);
    }

    @AfterThrowing(aopExp)
    public void afterThrowing() {
        System.out.println(&quot;afterThrowing ......&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;定義切點-point-cut&quot;&gt;定義切點 Point cut&lt;/h3&gt;
&lt;p&gt;剛剛的 Aspect0.java 每個註解都有相同的 regular expression，這邊改用一個 method 定義切點 point cut &lt;code&gt;@Pointcut(aopExp)&lt;/code&gt;，再用該 point cut 定義 AOP advices&lt;/p&gt;
&lt;p&gt;MyAspect.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.aspect;

import com.test.aspect.validator.UserValidator;
import com.test.aspect.validator.impl.UserValidatorImpl;
import com.test.aspect.pojo.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

// 宣告為 Aspect，要加上 Component 註冊到 IoC
@Aspect
@Component
public class MyAspect {
    // 以 regular expression，指定 join point (包含 class, method)
    // execution  代表在執行時，要攔截匹配的 regluar expression
    // * 代表不在意 method 回傳任何資料型別
    // com.test.aspect.service.impl.UserServiceImpl  指定 target object
    // printUser  指定 method
    // (..) 代表匹配任意參數
    private static final String aopExp = &quot;execution(* &quot; + &quot;com.test.aspect.service.impl.UserServiceImpl.printUser(..))&quot;;

//    // 定義 Enhancer bean 與 class
//    @DeclareParents(
//            // 需要引入增强的Bean
//            value = &quot;com.test.aspect.service.impl.UserServiceImpl&quot;,
//            // 用這個類別增強
//            defaultImpl = UserValidatorImpl.class)
//    // Enhancer interface
//    public UserValidator userValidator;

    // 用 @Pointcut 定義切點，後面的 advice 可用這個 method 指定 join point
    @Pointcut(aopExp)
    public void pointCut() {
    }

    @Before(&quot;pointCut()&quot;) // 用 point cut 定義
    public void before() {
        System.out.println(&quot;before ......&quot;);
    }

    // args(user) 代表要傳遞的參數
    @Before(&quot;pointCut() &amp;amp;&amp;amp; args(user)&quot;)
    public void beforeParam(JoinPoint jp, User user) {
        System.out.println(&quot;beforeParam ......&quot;);
    }

    @After(&quot;pointCut()&quot;) // 用 point cut 定義
    public void after() {
        System.out.println(&quot;after ......&quot;);
    }

    @AfterReturning(&quot;pointCut()&quot;) // 用 point cut 定義
    public void afterReturning() {
        System.out.println(&quot;afterReturning ......&quot;);
    }

    @AfterThrowing(&quot;pointCut()&quot;) // 用 point cut 定義
    public void afterThrowing() {
        System.out.println(&quot;afterThrowing ......&quot;);
    }

//    @Around(&quot;pointCut()&quot;)
//    public void around(ProceedingJoinPoint jp) throws Throwable {
//        System.out.println(&quot;around before......&quot;);
//        // call back target object method
//        jp.proceed();
//        System.out.println(&quot;around after......&quot;);
//    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 class=&quot;atx&quot; id=&quot;aspectj關於pointcut-的指示器&quot;&gt;&lt;code&gt;@AspectJ&lt;/code&gt;關於&lt;code&gt;@Pointcut&lt;/code&gt; 的指示器&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;類型&lt;/th&gt;
&lt;th&gt;desc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;arg()&lt;/td&gt;
&lt;td&gt;限制 join point method 的參數&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@args()&lt;/td&gt;
&lt;td&gt;以 join point method 參數的註解進行限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;execution()&lt;/td&gt;
&lt;td&gt;匹配 join point 的執行 method&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;this()&lt;/td&gt;
&lt;td&gt;匹配當前 AOP 代理物件類別的方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;target&lt;/td&gt;
&lt;td&gt;target object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@target()&lt;/td&gt;
&lt;td&gt;匹配當前 target object 類別的方法，target object 必須標注指定的註解&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;within&lt;/td&gt;
&lt;td&gt;限制 join point 匹配指定的類別&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@within()&lt;/td&gt;
&lt;td&gt;限制 join point 帶有匹配註解的類別&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@annotation&lt;/td&gt;
&lt;td&gt;限制帶有指定註解的join point&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;測試aop&quot;&gt;測試AOP&lt;/h3&gt;
&lt;p&gt;加上 AopConfig.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan(&quot;com.test.*&quot;)
@EnableAspectJAutoProxy // 啟動 AOP
public class AopConfig {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AopTest1Service.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.main;

import com.test.aspect.pojo.User;
import com.test.aspect.service.UserService;
import com.test.aspect.validator.UserValidator;
import com.test.config.AopConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopTest1Service {

    public static void main(String[] args) {
        testAop();
    }

    public static void testAop() {
        var ctx = new AnnotationConfigApplicationContext(AopConfig.class);
        try {
            var userService = ctx.getBean(UserService.class);
            var user = new User();
            user.setId(1L);
            user.setUserName(&quot;username1&quot;);
            user.setNote(&quot;note1&quot;);
            userService.printUser(user);
            System.out.println(&quot;########## exception test #####&quot;);
            userService.printUser(null);
        } finally {
            ctx.close();
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;before ......
user=User(id=1, userName=username1, note=note1)
afterReturning ......
after ......
########## exception test #####
before ......
afterThrowing ......
after ......
Exception in thread &quot;main&quot; java.lang.RuntimeException: user is null!&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;around-advice&quot;&gt;around advice&lt;/h3&gt;
&lt;p&gt;只有在需要大幅度修改原本 target object 的商業邏輯時，才需要使用 around，一般狀況下，不要使用 around&lt;/p&gt;
&lt;p&gt;修改 MyAspect.java 加上 around&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;    @Around(&quot;pointCut()&quot;)
    public void around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println(&quot;around before......&quot;);
        // call back target object method
        jp.proceed();
        System.out.println(&quot;around after......&quot;);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AopTest1Service.java 執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;around before......
before ......
beforeParam ......
user=User(id=1, userName=username1, note=note1)
afterReturning ......
after ......
around after......
########## exception test #####
around before......
before ......
beforeParam ......
afterThrowing ......
after ......&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;在使用 UserService 時，會因為 null 發生 exception，可利用 introduction 功能增強，而不需要修改原本的 UserServiceImpl&lt;/p&gt;
&lt;p&gt;UserValidator.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.aspect.validator;

import com.test.aspect.pojo.User;

public interface UserValidator {
    // testing user object is null or not
    public boolean validate(User user);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UserValidatorImpl.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.aspect.validator.impl;

import com.test.aspect.validator.UserValidator;
import com.test.aspect.pojo.User;

public class UserValidatorImpl implements UserValidator {
    @Override
    public boolean validate(User user) {
        System.out.println(&quot;validate user with class: &quot; + UserValidator.class.getSimpleName());
        return user != null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改剛剛的 MyAspect.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;    // 定義 Enhancer bean 與 class
    @DeclareParents(
            // 需要 introduction 增强的Bean
            value = &quot;com.test.aspect.service.impl.UserServiceImpl&quot;,
            // 用這個類別增強
            defaultImpl = UserValidatorImpl.class)
    // Enhancer interface
    public UserValidator userValidator;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;測試&lt;/p&gt;
&lt;p&gt;將 userService 轉換為 UserValidator 後使用 validate()&lt;/p&gt;
&lt;p&gt;AopTest2Introduction.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.main;

import com.test.aspect.pojo.User;
import com.test.aspect.service.UserService;
import com.test.aspect.validator.UserValidator;
import com.test.config.AopConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopTest2Introduction {

    public static void main(String[] args) {
        testIntroduction();
    }

    public static void testIntroduction() {
        var ctx = new AnnotationConfigApplicationContext(AopConfig.class);
        try {
            var userService = ctx.getBean(UserService.class);
            var user = new User();
            user.setId(1L);
            user.setUserName(&quot;username1&quot;);
            user.setNote(&quot;note1&quot;);
            // convert to UserValidator interface
            var userValidator = (UserValidator) userService;
            // 检查user is null ot not
            if (userValidator.validate(user)) {
                // 檢查通過，列印user 資訊
                userService.printUser(user);
            }

        } finally {
            ctx.close();
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果，就不會發生 exception&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;validate user with class: UserValidator
before ......
beforeParam ......
user=User(id=1, userName=username1, note=note1)
afterReturning ......
after ......&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;advice-parameter&quot;&gt;advice parameter&lt;/h3&gt;
&lt;p&gt;要給 advice 通知加上參數，只需要修改 regular expression 與指示器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;    // args(user) 代表要傳遞的參數
    @Before(&quot;pointCut() &amp;amp;&amp;amp; args(user)&quot;)
    public void beforeParam(JoinPoint jp, User user) {
        System.out.println(&quot;beforeParam ......&quot;);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;weaving&quot;&gt;weaving&lt;/h3&gt;
&lt;p&gt;先用一個 interface 再加上實作，是 spring 推薦的方式，但 AOP 並不一定只能用在 interface。&lt;/p&gt;
&lt;p&gt;動態代理有 JDK, cglib 及 JDK, javaassist。JDK 動態代理是要求代理對象必須要是 interface，但 cglib 沒有限制。預設狀況，如果有 interface，spring 會使用 JDK 動態代理，如果沒有，就用 cglib。&lt;/p&gt;
&lt;p&gt;spring boot 預設就是使用 cglib。上面的 AopConfig.java 如果加上 &lt;code&gt;@SpringBootApplication&lt;/code&gt; 就代表這是 spring boot&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-properties&quot;&gt;# false: 採用原本 spring 的方法
# true: 預設值，使用 spring boot 的方法，永遠使用 cglib
spring.aop.proxy-target-class=false&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;多個切面&quot;&gt;多個切面&lt;/h2&gt;
&lt;p&gt;AOP 可同時對一個 method 支援多個 aspects。&lt;/p&gt;
&lt;p&gt;首先定義三個 Aspects&lt;/p&gt;
&lt;p&gt;MyAspect1.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.aspect.multi;

import org.aspectj.lang.annotation.*;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
//@Order(3)
public class MyAspect1 implements Ordered {

    // 順序
    @Override
    public int getOrder() {
        return 3;
    }

    private static final String exp = &quot;execution(* &quot; + &quot;com.test.aspect.service.impl.UserServiceImpl.multiAspects(..))&quot;;

    @Pointcut(exp)
    public void multiAspects() {
    }

    @Before(&quot;multiAspects()&quot;)
    public void before() {
        System.out.println(&quot;MyAspect1 before ......&quot;);
    }

    @After(&quot;multiAspects()&quot;)
    public void after() {
        System.out.println(&quot;MyAspect1 after ......&quot;);
    }

    @AfterReturning(&quot;multiAspects()&quot;)
    public void afterReturning() {
        System.out.println(&quot;MyAspect1 afterReturning ......&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MyApsect2.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.aspect.multi;

import org.aspectj.lang.annotation.*;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
//@Order(2)
public class MyAspect2 implements Ordered {

    // 順序
    @Override
    public int getOrder() {
        return 2;
    }

    private static final String exp = &quot;execution(* &quot; + &quot;com.test.aspect.service.impl.UserServiceImpl.multiAspects(..))&quot;;

    @Pointcut(exp)
    public void multiAspects() {
    }

    @Before(&quot;multiAspects()&quot;)
    public void before() {
        System.out.println(&quot;MyAspect2 before ......&quot;);
    }

    @After(&quot;multiAspects()&quot;)
    public void after() {
        System.out.println(&quot;MyAspect2 after ......&quot;);
    }

    @AfterReturning(&quot;multiAspects()&quot;)
    public void afterReturning() {
        System.out.println(&quot;MyAspect2 afterReturning ......&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MyAspect3.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.aspect.multi;

import org.aspectj.lang.annotation.*;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(1)
public class MyAspect3 {

//    // 順序
//    @Override
//    public int getOrder() {
//        return 1;
//    }

    private static final String exp = &quot;execution(* &quot; + &quot;com.test.aspect.service.impl.UserServiceImpl.multiAspects(..))&quot;;

    @Pointcut(exp)
    public void multiAspects() {
    }

    @Before(&quot;multiAspects()&quot;)
    public void before() {
        System.out.println(&quot;MyAspect3 before ......&quot;);
    }

    @After(&quot;multiAspects()&quot;)
    public void after() {
        System.out.println(&quot;MyAspect3 after ......&quot;);
    }

    @AfterReturning(&quot;multiAspects()&quot;)
    public void afterReturning() {
        System.out.println(&quot;MyAspect3 afterReturning ......&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Aspects 的執行先後順序是用 &lt;code&gt;@Order&lt;/code&gt; 或是 override &lt;code&gt;Order&lt;/code&gt; interface 的 &lt;code&gt;public int getOrder()&lt;/code&gt;，上面設定執行順序為 MyAspect3, MyAspect2, MyAspect1&lt;/p&gt;
&lt;p&gt;AopTest3MultiAspect.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.main;

import com.test.aspect.pojo.User;
import com.test.aspect.service.UserService;
import com.test.aspect.validator.UserValidator;
import com.test.config.AopConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopTest3MultiAspect {

    public static void main(String[] args) {
        testMultiAspects();
    }

    public static void testMultiAspects() {
        var ctx = new AnnotationConfigApplicationContext(AopConfig.class);
        try {
            var userService = ctx.getBean(UserService.class);
            // 測試 multiple aspects
            userService.multiAspects();
        } finally {
            ctx.close();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;測試結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;MyAspect3 before ......
MyAspect2 before ......
MyAspect1 before ......
testing multiAspects
MyAspect1 afterReturning ......
MyAspect1 after ......
MyAspect2 afterReturning ......
MyAspect2 after ......
MyAspect3 afterReturning ......
MyAspect3 after ......&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/6015275589961326683/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/07/spring-aop.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/6015275589961326683'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/6015275589961326683'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/07/spring-aop.html' title='Spring AOP'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-7788081033857395879</id><published>2026-06-29T08:40:25.448+08:00</published><updated>2026-06-29T08:40:25.448+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>Spring IoC</title><content type='html'>&lt;p&gt;需要管理的物件對象稱為 Spring Bean，Bean 的管理器稱為 Spring IoC Container。IoC container 有兩個功能&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;管理 bean 的定義、發布、設定、銷毀&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;描述 bean 之間的依賴關係&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有 IoC Container 都需要實作 BeanFactory Interface，主要的 method：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;getBean&lt;/p&gt;
&lt;p&gt;依照名稱/類別，取得 bean&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;Object getBean(String name) throws BeansException;
&amp;lt;T&amp;gt; T getBean(String name, Class&amp;lt;T&amp;gt; requiredType) throws BeansException;
&amp;lt;T&amp;gt; T getBean(Class&amp;lt;T&amp;gt; requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
&amp;lt;T&amp;gt; T getBean(Class&amp;lt;T&amp;gt; requiredType, Object... args) throws BeansException;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;getBeanProvider&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;&amp;lt;T&amp;gt; ObjectProvider&amp;lt;T&amp;gt; getBeanProvider(Class&amp;lt;T&amp;gt; requiredType);
&amp;lt;T&amp;gt; ObjectProvider&amp;lt;T&amp;gt; getBeanProvider(ResolvableType requiredType);&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;containsBean&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;boolean containsBean(String name);&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;isSingleton&lt;/p&gt;
&lt;p&gt;IoC 裡面，bean 預設都是 singleton，getBean 預設都會回傳同一個 bean&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;boolean isSingleton(String name) throws NoSuchBeanDefinitionException;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;isPrototype&lt;/p&gt;
&lt;p&gt;getBean 會透過 IoC 產生新的 bean 回傳&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;boolean isPrototype(String name) throws NoSuchBeanDefinitionException;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;isTypeMatch&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, Class&amp;lt;?&amp;gt; typeToMatch) throws NoSuchBeanDefinitionException;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;getType&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;Class&amp;lt;?&amp;gt; getType(String name) throws NoSuchBeanDefinitionException;
Class&amp;lt;?&amp;gt; getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;getAliases&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;String[] getAliases(String name);&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;spring 在 BeanFactory 基礎上，加上 ApplicationContext，實際的 application 大部分都是使用 ApplicationContext 的 interface&lt;/p&gt;
&lt;hr&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;component-vs-bean&quot;&gt;@Component vs @Bean&lt;/h2&gt;
&lt;p&gt;ref: &lt;a href=&quot;https://javaniuniu.com/Annotation/ComponentAndBean&quot;&gt;注解中@Component和@Bean的区别 - JAVA 牛牛&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;@Component（@Controller、@Service、@Repository）是透過 ComponentScan 掃描來做自動偵測，並自動設定到IoC。@Component 只是一個類別的定義，告知 IoC 要產生這個類別的 bean&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;在 @Compoent 類別的屬性裡面，加上 @Value，可直接設定該欄位的值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;@Bean 是使用在 method，在有該 annotation 的 method 會回傳一個 bean 的物件。該 method 裡面是產生 bean 的邏輯。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import lombok.Data;
import org.springframework.stereotype.Component;

@Data
@Component(&quot;user&quot;)
//@Scope(&quot;prototype&quot;)
public class User {
    private Long id;
    private String userName;
    private String note;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Data
@Component(&quot;admin&quot;)
public class Admin {

    @Value(&quot;1&quot;)
    private Long id;
    @Value(&quot;user_name_1&quot;)
    private String userName;
    @Value(&quot;note_1&quot;)
    private String note;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AppConfig.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;@Configuration
@ComponentScan(basePackages = &quot;com.test.ioc.*&quot;
//        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes = Service.class)
)
public class AppConfig {
    // name 代表 Bean名稱
    @Bean(name = &quot;user2&quot;)
    public User initUser() {
        var user = new User();
        user.setId(2L);
        user.setUserName(&quot;user_name_2&quot;);
        user.setNote(&quot;note_2&quot;);
        return user;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;測試&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;public class IoCTest {

    public static void main(String[] args) {
        var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        try {
            var admimbean1 = ctx.getBean(Admin.class);
            var admimbean2 = ctx.getBean(Admin.class);
            var admimbean3 = ctx.getBean(&quot;admin&quot;);
            System.out.println(&quot;Admin: admimbean1, name=&quot; + admimbean1.getUserName());
            System.out.println(&quot;Admin 2==3:&quot; + (admimbean1 == admimbean2)+&quot;, name=&quot;+admimbean2.getUserName());
            System.out.println(&quot;Admin 1==3:&quot; + (admimbean1 == admimbean3)+&quot;, name=&quot;+((Admin)admimbean3).getUserName());

            var userbaen1 = ctx.getBean(&quot;user&quot;);
            var userbaen2 = ctx.getBean(&quot;user2&quot;);
//            var userbaen3 = ctx.getBean(User.class);
            System.out.println(&quot;User: userbaen1, name=&quot; + ((User)userbaen1).getUserName());
            System.out.println(&quot;User: userbaen2, name=&quot; + ((User)userbaen2).getUserName());
//            System.out.println(&quot;User: userbaen3, name=&quot; + ((User)userbaen3).getUserName());
            System.out.println(&quot;User 1==2:&quot; + (userbaen1 == userbaen2)+&quot;, name=&quot;+((User)userbaen1).getUserName());
//            System.out.println(&quot;User 2==3:&quot; + (userbaen2 == userbaen3)+&quot;, name=&quot;+((User)userbaen3).getUserName());
        } finally {
            ctx.close();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;Admin: admimbean1, name=user_name_1
Admin 2==3:true, name=user_name_1
Admin 1==3:true, name=user_name_1
User: userbaen1, name=null
User: userbaen2, name=user_name_2
User 1==2:false, name=null&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;@ComponentScan 可使用的參數&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;basePackages&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;basePackageClasses&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;includeFilters&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;excludeFilters&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;lazyInit&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;includeFilters, excludeFilters 都需要有 @ComponentScan.Filter 定義&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;type: 選擇 filter type: ANNOTATION/ASSIGNABLE_TYPE/ASPECTJ/REGEX/CUSTOM&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;classes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;pattern&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ex:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;@ComponentScan(basePackages = &quot;com.test.ioc.*&quot;,
        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes = Service.class)
)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;@ComponentScan(basePackages = {&quot;com.test.ioc.*&quot;})

@ComponentScan(&quot;com.test.ioc.*&quot;)

@ComponentScan(basePackageClasses = {&quot;User.class&quot;})&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;library-bean&quot;&gt;library bean&lt;/h2&gt;
&lt;p&gt;使用第三方函式庫的物件時，可透過 &lt;code&gt;@Bean&lt;/code&gt; 產生 bean 放到 IoC container 管理&lt;/p&gt;
&lt;p&gt;pom.xml 使用 mysql jdbc driver&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-xml&quot;&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.mysql&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mysql-connector-j&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在剛剛的 AppConfig.java 加上 dataSource 定義&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;    // @Bean 產生物件，name為 名稱
    @Bean(name = &quot;dataSource&quot;)
    public DataSource getDataSource() {
        var dataSource = new MysqlDataSource();
        try {
            dataSource.setUrl(&quot;jdbc:mysql://localhost:3306/testweb&quot;);
            dataSource.setUser(&quot;root&quot;);
            dataSource.setPassword(&quot;password&quot;);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回到 IoCTest.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;    public static void main(String[] args) {
        var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        try {
            testDatasource( (DataSource)ctx.getBean(&quot;dataSource&quot;) );
        } finally {
            ctx.close();
        }
    }

    public static void testDatasource(DataSource ds) {
        System.out.println(&quot;testDatasource&quot;);
        try (Connection con = ds.getConnection();
             PreparedStatement ps = createPreparedStatement(con, 1);
             ResultSet rs = ps.executeQuery()) {
            // process the resultset here, all resources will be cleaned up
            while (rs.next()) {
                System.out.print(&quot;id:&quot; + rs.getInt(1));
                System.out.print(&quot;,username:&quot; + rs.getString(2));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    private static PreparedStatement createPreparedStatement(Connection con, int userId) throws SQLException {
        String sql = &quot;SELECT id, username FROM user WHERE id = ?&quot;;
        PreparedStatement ps = con.prepareStatement(sql);
        ps.setInt(1, userId);
        return ps;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;testDatasource
id:1,username:Lily&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;dependency-injection&quot;&gt;Dependency Injection&lt;/h2&gt;
&lt;p&gt;先定義 兩個 interface&lt;/p&gt;
&lt;p&gt;Animal.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.dependency.pojo;

public interface Animal {
    public void use();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Person.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.dependency.pojo;

public interface Person {
    public void service();
    public void setAnimal(Animal animal);

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AnimalDog.java implements Animal&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.dependency.pojo.impl;

import com.test.ioc.dependency.pojo.Animal;
import org.springframework.stereotype.Component;

@Component
public class AnimalDog implements Animal {

    @Override
    public void use() {
        System.out.println(&quot;Dog &quot; + AnimalDog.class.getSimpleName() + &quot; for help.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PersonSales.java&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;@Autowired&lt;/code&gt; 自動綁定一個 animal 物件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.dependency.pojo.impl;

import com.test.ioc.dependency.pojo.Animal;
import com.test.ioc.dependency.pojo.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class PersonSales implements Person {
    @Autowired
    private Animal animal = null;

    @Override
    public void service() {
        this.animal.use();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AppConfig2.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.dependency;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = &quot;com.test.ioc.dependency.*&quot;)
public class AppConfig2 {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IoCTest2.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.dependency;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.test.ioc.dependency.pojo.impl.PersonSales;

public class IoCTest2 {

    public static void main(String[] args) {
        var ctx = new AnnotationConfigApplicationContext(AppConfig2.class);
        try {
            var person = ctx.getBean(PersonSales.class);
            person.service();
        } finally {
            ctx.close();
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;Dog AnimalDog for help.&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;如果再增加一個 AnimalCat&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.dependency.pojo.impl;

import com.test.ioc.dependency.pojo.Animal;
import org.springframework.stereotype.Component;

@Component
public class AnimalCat implements Animal {
    @Override
    public void use() {
        System.out.println(&quot;Cat &quot; + AnimalCat.class.getSimpleName() + &quot; for rats&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行後會發生 Exception&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;Exception in thread &quot;main&quot; org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name &#39;personSales&#39;:
Unsatisfied dependency expressed through field &#39;animal&#39;:
No qualifying bean of type &#39;com.test.ioc.dependency.pojo.Animal&#39; available:
expected single matching bean but found 2: animalCat,animalDog&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因為在 &lt;code&gt;@AutoWired&lt;/code&gt; 時，只使用了 animal 屬性，但 container 不確定要使用 Cat 還是 Dog&lt;/p&gt;
&lt;p&gt;如果將 animal 改成 animalDog，就很明確。因為 &lt;code&gt;@Autowired&lt;/code&gt; 會先根據類別產生 bean，然後再根據屬性名稱匹配。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;@Component
public class PersonSales implements Person {
    @Autowired
    private Animal animalDog = null;

    @Override
    public void service() {
        this.animalDog.use();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animalDog = animal;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@Autowired&lt;/code&gt; 也可以寫在 method 上面&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;    @Override
    @Autowired
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;primary-與-qualifier&quot;&gt;&lt;code&gt;@Primary&lt;/code&gt; 與 &lt;code&gt;@Qualifier&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;在 AnimalCat 加上 &lt;code&gt;@Primary&lt;/code&gt; 就看可以確定先使用這個類別&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;@Component
@Primary
public class AnimalCat implements Animal {
    @Override
    public void use() {
        System.out.println(&quot;Cat &quot; + AnimalCat.class.getSimpleName() + &quot; for rats&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但如果又在 AnimalDog 加上 &lt;code&gt;@Primary&lt;/code&gt;，還是會發生問題。&lt;/p&gt;
&lt;p&gt;所以應該在使用 bean 的地方，用 &lt;code&gt;@Qualifier&lt;/code&gt; 限制應該要用哪一個 bean&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;    @Override
    @Autowired
    @Qualifier(&quot;animalDog&quot;)
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;在 class constructor 的參數，也可使用 &lt;code&gt;@Autowired&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;@Component
public class PersonSales implements Person {

    private Animal animal = null;

    public PersonSales(@Autowired @Qualifier(&quot;animalDog&quot;) Animal animal) {
        this.animal = animal;
    }

    @Override
    public void service() {
        this.animal.use();
    }

    @Override
//    @Autowired
//    @Qualifier(&quot;animalDog&quot;)
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;bean-的生命週期&quot;&gt;Bean 的生命週期&lt;/h2&gt;
&lt;p&gt;bean 的初始化流程&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-mermaid&quot;&gt;flowchart TB
    A[&quot;資源定位\n使用 @ComponentScan&quot;] --&amp;gt; B[&quot;Bean 的定義\n存到 BeanDefinition&quot;]
    B --&amp;gt; C[&quot;發布 bean 的定義\nIoC Container 載入定義&quot;]
    C --&amp;gt; D[&quot;實例化\n產生 bean object&quot;]
    D --&amp;gt; E[&quot;DI\n使用 @Autowired 注入資源&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@ComponentScan&lt;/code&gt; 裡面有個 lazyInit 參數，預設為 false，預設不會延遲 bean 初始化，而是在 getBean 時，才會初始化&lt;/p&gt;
&lt;p&gt;修改 AppConfig2.java 加上 lazyInit = true&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;@Configuration
@ComponentScan(basePackages = &quot;com.test.ioc.dependency.*&quot;, lazyInit = true)
public class AppConfig2 {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spring 在完成 DI 以後，會依照此流程管理 bean 的 life cycle&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-mermaid&quot;&gt;flowchart TB
A[&quot;初始化&quot;] --&amp;gt; B[&quot;DI&quot;]
B --&amp;gt; C[&quot;setBeanName()\n介面 BeanNameAware&quot;]
C --&amp;gt; D[&quot;setBeanFactory()\n介面 BeanFactoryAware&quot;]
D --&amp;gt; E[&quot;setAppicationContext()\n介面 ApplicationContextAware\n容器要實作ApplicationContext才會被呼叫&quot;]
E --&amp;gt; F[&quot;postProcessBeforeInitialization()\n BeanPostProcessor method&quot;]
F --&amp;gt; G[&quot;自訂初始化 method\n@PostConstruct method&quot;]
G --&amp;gt; H[&quot;afterPropertiesSet()\n介面 InitializingBean&quot;]
H --&amp;gt; I[&quot;postProcessAfterInitialization()\nBeanPostProcessor method&quot;]
I --&amp;gt; J[&quot;bean 的使用期間&quot;]
J --&amp;gt; K[&quot;自訂銷毀 method\n@PreDestory method&quot;]
K --&amp;gt; L[&quot;destory method\n介面 DisposableBean&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IoC container 基本是需要實作 BeanFactory 介面，不一定要實作 ApplicationContext&lt;/p&gt;
&lt;p&gt;修改 PersonBussiness.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.dependency.pojo.impl;

import com.test.ioc.dependency.pojo.Animal;
import com.test.ioc.dependency.pojo.Person;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class PersonBussiness implements Person, BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean {

    private Animal animal = null;

    @Override
    public void service() {
        this.animal.use();
    }

    @Override
    @Autowired
    @Qualifier(&quot;animalDog&quot;)
    public void setAnimal(Animal animal) {
        System.out.println(&quot;lazyInit&quot;);
        this.animal = animal;
    }

    @Override
    public void setBeanName(String beanName) {
        System.out.println(&quot;【&quot; + this.getClass().getSimpleName() + &quot;】invoke BeanNameAware.setBeanName&quot;);
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println(&quot;【&quot; + this.getClass().getSimpleName() + &quot;】invoke BeanFactoryAware.setBeanFactory&quot;);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println(&quot;【&quot; + this.getClass().getSimpleName() + &quot;】invoke ApplicationContextAware.setApplicationContext&quot;);

    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println(&quot;【&quot; + this.getClass().getSimpleName() + &quot;】invoke InitializingBean.afterPropertiesSet&quot;);
    }

    @PostConstruct
    public void init() {
        System.out.println(&quot;【&quot; + this.getClass().getSimpleName() + &quot;】@PostConstruct 定義的自訂初始化方法&quot;);
    }

    @PreDestroy
    public void destroy1() {
        System.out.println(&quot;【&quot; + this.getClass().getSimpleName() + &quot;】@PreDestroy 定義的自訂銷毀方法&quot;);
    }

    @Override
    public void destroy() throws Exception {
        System.out.println(&quot;【&quot; + this.getClass().getSimpleName() + &quot;】 DisposableBean&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IoCTest2.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;public class IoCTest2 {

    public static void main(String[] args) {
        var ctx = new AnnotationConfigApplicationContext(AppConfig2.class);
        try {
            var person = ctx.getBean(PersonBussiness.class);
            person.service();
        } finally {
            ctx.close();
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;lazyInit
【PersonBussiness】invoke BeanNameAware.setBeanName
【PersonBussiness】invoke BeanFactoryAware.setBeanFactory
【PersonBussiness】invoke ApplicationContextAware.setApplicationContext
【PersonBussiness】@PostConstruct 定義的自訂初始化方法
【PersonBussiness】invoke InitializingBean.afterPropertiesSet
Dog AnimalDog for help.
【PersonBussiness】@PreDestroy 定義的自訂銷毀方法
【PersonBussiness】 DisposableBean&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;增加 BeanPostProcessorExample.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.dependency.life;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class BeanPostProcessorExample implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println(&quot;BeanPostProcessor invoke &quot; + &quot;postProcessBeforeInitialization method  參數【&quot; + bean.getClass().getSimpleName() + &quot;】【&quot; + beanName + &quot;】 &quot;);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println(&quot;BeanPostProcessor invoke &quot; + &quot;postProcessAfterInitialization method  參數【&quot; + bean.getClass().getSimpleName() + &quot;】【&quot; + beanName + &quot;】 &quot;);
        return bean;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;BeanPostProcessor invoke postProcessBeforeInitialization method  參數【AppConfig2$$SpringCGLIB$$0】【appConfig2】 
BeanPostProcessor invoke postProcessAfterInitialization method  參數【AppConfig2$$SpringCGLIB$$0】【appConfig2】 
BeanPostProcessor invoke postProcessBeforeInitialization method  參數【AnimalDog】【animalDog】 
BeanPostProcessor invoke postProcessAfterInitialization method  參數【AnimalDog】【animalDog】 
lazyInit
【PersonBussiness】invoke BeanNameAware.setBeanName
【PersonBussiness】invoke BeanFactoryAware.setBeanFactory
【PersonBussiness】invoke ApplicationContextAware.setApplicationContext
BeanPostProcessor invoke postProcessBeforeInitialization method  參數【PersonBussiness】【personBussiness】 
【PersonBussiness】@PostConstruct 定義的自訂初始化方法
【PersonBussiness】invoke InitializingBean.afterPropertiesSet
BeanPostProcessor invoke postProcessAfterInitialization method  參數【PersonBussiness】【personBussiness】 
Dog AnimalDog for help.
【PersonBussiness】@PreDestroy 定義的自訂銷毀方法
【PersonBussiness】 DisposableBean&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;properties-file&quot;&gt;Properties file&lt;/h2&gt;
&lt;p&gt;spring boot 通常使用一個 application.properties 檔案處理設定&lt;/p&gt;
&lt;p&gt;先在 pom.xml 加上 spring-boot-configuration-processor，就可以直接使用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-configuration-processor&amp;lt;/artifactId&amp;gt;
  &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 SpEL 語法，可參考到 database.properties 設定&lt;/p&gt;
&lt;p&gt;DataBaseProperties.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.prop.props;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@Data
public class DataBaseProperties {
    @Value(&quot;${database.driverName}&quot;)
    private String driverName = null;
    @Value(&quot;${database.url}&quot;)
    private String url = null;
    @Value(&quot;${database.username}&quot;)
    private String username = null;
    @Value(&quot;${database.password}&quot;)
    private String password = null;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以直接用 &lt;code&gt;@ConfigurationProperties(&quot;database&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.prop.props;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(&quot;database&quot;)
@Data
public class DataBaseProperties2 {
    private String driverName = null;
    private String url = null;
    private String username = null;
    private String password = null;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IoCTest3.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.prop;

import com.test.ioc.prop.props.DataBaseProperties;
import com.test.ioc.prop.props.DataBaseProperties2;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class IoCTest3 {

    public static void main(String[] args) {
        var ctx = new AnnotationConfigApplicationContext(AppConfig3.class);
        try {
            var prop1 = ctx.getBean(DataBaseProperties.class);
            System.out.println(&quot;prop1 url=&quot;+prop1.getUrl());
            var prop2 = ctx.getBean(DataBaseProperties2.class);
            System.out.println(&quot;prop2 url=&quot;+prop2.getUrl());
        } finally {
            ctx.close();
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;prop1 url=jdbc:mysql://localhost:3306/testweb
prop2 url=jdbc:mysql://localhost:3306/testweb&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;conditional&quot;&gt;@Conditional&lt;/h2&gt;
&lt;p&gt;有時候會遇到缺少一些參數，讓 bean 無法初始化，可透過 &lt;code&gt;@Conditional&lt;/code&gt; 限制 bean 的條件&lt;/p&gt;
&lt;p&gt;DatabaseConditional.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.prop.conditional;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class DatabaseConditional implements Condition {

    /**
     * data source condition
     * @param context
     * @param metadata
     * @return true -&amp;gt; init bean
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 取得 env
        var env = context.getEnvironment();
        // 判断 .properties 內容，是否有相關參數
        return env.containsProperty(&quot;database.url&quot;)
                &amp;amp;&amp;amp; env.containsProperty(&quot;database.username&quot;)
                &amp;amp;&amp;amp; env.containsProperty(&quot;database.password&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PropApplication.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;@SpringBootApplication(scanBasePackages = &quot;com.test.ioc.prop&quot;)
@PropertySource(value = &quot;classpath:jdbc.properties&quot;, ignoreResourceNotFound = true)
@EnableConfigurationProperties
public class PropApplication {

    public static void main(String[] args) {
        SpringApplication.run(PropApplication.class, args);
    }

    @Bean(name = &quot;dataSource2&quot;)
    // 透過 DatabaseConditional 限制 bean 的條件
    @Conditional(DatabaseConditional.class)
    public DataSource getDataSource(
            @Value(&quot;${database.url}&quot;) String url,
            @Value(&quot;${database.username}&quot;) String username,
            @Value(&quot;${database.password}&quot;) String password
    ) {
        System.out.println(&quot;init datasource&quot;);
        var dataSource = new MysqlDataSource();
        dataSource.setUrl(url);
        dataSource.setUser(username);
        dataSource.setPassword(password);
        return dataSource;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 database.properties 缺少了一些參數，也不會讓 PropApplication 無法啟動&lt;/p&gt;
&lt;hr&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;bean-scope&quot;&gt;Bean Scope&lt;/h2&gt;
&lt;p&gt;一般來說，bean 有 singleton, prototype 兩種，在 Jakarta EE 的 web container 裡面，還增加了 page, reqeust, session, application 四種 scope&lt;/p&gt;
&lt;p&gt;page scope 作用範圍是 JSP，Spring 沒有支援&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;scope&lt;/th&gt;
&lt;th&gt;使用範圍&lt;/th&gt;
&lt;th&gt;desc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;singleton&lt;/td&gt;
&lt;td&gt;spring application&lt;/td&gt;
&lt;td&gt;預設值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prototype&lt;/td&gt;
&lt;td&gt;spring application&lt;/td&gt;
&lt;td&gt;從 IoC container 取得一個 bean 都是產生新的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;session&lt;/td&gt;
&lt;td&gt;spring web application&lt;/td&gt;
&lt;td&gt;HTTP session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;application&lt;/td&gt;
&lt;td&gt;spring web application&lt;/td&gt;
&lt;td&gt;web application life cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;request&lt;/td&gt;
&lt;td&gt;spring web application&lt;/td&gt;
&lt;td&gt;web application http request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;globalSession&lt;/td&gt;
&lt;td&gt;spring web application&lt;/td&gt;
&lt;td&gt;global http session，一個 bean 對應一個 object，不常使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;ScopBean.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.scope;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ScopeBean {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)&lt;/code&gt; 是定義為 prototype&lt;/p&gt;
&lt;p&gt;如果是 spring web application 還可以使用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;WebApplicationContext.SCOPE_REQUEST&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WebApplicationContext.SCOPE_SESSION&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WebApplicationContext.SCOPE_APPLICATION&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;profile&quot;&gt;@Profile&lt;/h2&gt;
&lt;p&gt;專案開發時，可能會有開發、測試、正式測試、正式環境這些不同的運作環境，不同的環境有不同的設定。&lt;/p&gt;
&lt;p&gt;假設有 dev, test 兩種資料庫，可使用 &lt;code&gt;@Profile&lt;/code&gt; 切換&lt;/p&gt;
&lt;p&gt;ScopeApplication.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;package com.test.ioc.scope;

import com.mysql.cj.jdbc.MysqlDataSource;
import com.test.ioc.prop.conditional.DatabaseConditional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;

import javax.sql.DataSource;

@SpringBootApplication(scanBasePackages = &quot;com.test.ioc.scope&quot;)
@PropertySource(value = &quot;classpath:jdbc.properties&quot;, ignoreResourceNotFound = true)
@EnableConfigurationProperties
public class ScopeApplication {
    public static void main(String[] args) {
        SpringApplication.run(ScopeApplication.class, args);
    }

    @Bean(name = &quot;dataSource&quot;)
    @Profile(&quot;dev&quot;)
    public DataSource getDevDataSource() {
        System.out.println(&quot;getDevDataSource&quot;);
        var dataSource = new MysqlDataSource();
        dataSource.setUrl(&quot;jdbc:mysql://localhost:3306/dev_testweb&quot;);
        dataSource.setUser(&quot;root&quot;);
        dataSource.setPassword(&quot;password&quot;);
        return dataSource;
    }

    @Bean(name = &quot;dataSource&quot;)
    @Profile(&quot;test&quot;)
    public DataSource getTestDataSource() {
        System.out.println(&quot;getTestDataSource&quot;);
        var dataSource = new MysqlDataSource();
        dataSource.setUrl(&quot;jdbc:mysql://localhost:3306/test_testweb&quot;);
        dataSource.setUser(&quot;root&quot;);
        dataSource.setPassword(&quot;password&quot;);
        return dataSource;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在啟動時切換&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;JAVA_OPTS=&quot;-Dspring.profiles.active=dev&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;spring 會自動使用 application-{profile}.properties&lt;/p&gt;
&lt;hr&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;spel&quot;&gt;SpEL&lt;/h2&gt;
&lt;p&gt;讀取 properties 的值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;@Value(&quot;${database.driverName}&quot;)
String driver;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;記錄 bean 的初始化時間&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;@Value(&quot;#{T(System).currentTimeMillis()}&quot;)
private Long initTime = null;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接設定屬性&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;    @Value(&quot;#{&#39;teststring&#39;}&quot;)
    private String str=null;

    @Value(&quot;#{3.1E2}&quot;)
    private double d;

    @Value(&quot;#{3.14}&quot;)
    private double e;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;取得其他 bean 的屬性&lt;/p&gt;
&lt;p&gt;&lt;code&gt;?.&lt;/code&gt; 是 null 判斷&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;    @Value(&quot;#{bean2.str}&quot;)
    private String otherStr=null;

    @Value(&quot;#{bean2.str?.toUpperCase()}&quot;)
    private String otherStrUpperCase=null;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;運算&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;@Value(&quot;#{1+2}&quot;)
private int run;

// 浮點數比較運算
@Value(&quot;#{beanName.pi == 3.14f}&quot;)
private boolean piFlag;

// 字串比較運算
@Value(&quot;#{beanName.str eq &#39;Spring Boot&#39;}&quot;)
private boolean strFlag;

// 字串連接
@Value(&quot;#{beanName.str + &#39; 連接字串&#39;}&quot;)
private String strApp = null;

// 三元運算
@Value(&quot;#{beanName.d &amp;gt; 1000 ? &#39;大於&#39; : &#39;小於&#39;}&quot;)
private String resultDesc = null;&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/7788081033857395879/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/06/spring-ioc.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/7788081033857395879'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/7788081033857395879'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/06/spring-ioc.html' title='Spring IoC'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-181471462893373429</id><published>2026-06-22T08:39:56.447+08:00</published><updated>2026-06-22T08:39:56.447+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>密碼 hash 演算法比較</title><content type='html'>&lt;p&gt;比較密碼加密演算法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;演算法&lt;/th&gt;
&lt;th&gt;可調整參數&lt;/th&gt;
&lt;th&gt;記憶體強度&lt;/th&gt;
&lt;th&gt;抗 GPU/ASIC&lt;/th&gt;
&lt;th&gt;密碼儲存&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;UnixCrypt&lt;/td&gt;
&lt;td&gt;無&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;無&lt;/td&gt;
&lt;td&gt;不適合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MD5&lt;/td&gt;
&lt;td&gt;無&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;無&lt;/td&gt;
&lt;td&gt;不適合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PBKDF2&lt;/td&gt;
&lt;td&gt;iterations&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;弱&lt;/td&gt;
&lt;td&gt;一般安全性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bcrypt&lt;/td&gt;
&lt;td&gt;cost factor&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;部分&lt;/td&gt;
&lt;td&gt;一般網站，推薦使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scrypt&lt;/td&gt;
&lt;td&gt;N, r, p&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;高安全性，錢包，加密金鑰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argon2&lt;/td&gt;
&lt;td&gt;time, mem, parallelism&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;最高&lt;/td&gt;
&lt;td&gt;最高安全性，密碼系統&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;java-profiler&quot;&gt;java profiler&lt;/h2&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-xml&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;
         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;groupId&amp;gt;com.maxkit.test&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;test&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0&amp;lt;/version&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;17&amp;lt;/maven.compiler.source&amp;gt;
        &amp;lt;maven.compiler.target&amp;gt;17&amp;lt;/maven.compiler.target&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.mindrot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;jbcrypt&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;0.4&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;at.favre.lib&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;bcrypt&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;0.10.2&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;!-- scrypt, Argon2 --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.bouncycastle&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;bcprov-jdk18on&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.82&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&amp;lt;/project&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.generators.SCrypt;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.mindrot.jbcrypt.BCrypt;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;

public class PasswordHashBenchmarkWithVerify {
    private static final String PASSWORD = &quot;MyPassword123!&quot;;
    private static final SecureRandom RANDOM = new SecureRandom();

    public static void main(String[] args) throws Exception {
        System.out.println(&quot;=== Hash + Verify Benchmark ===&quot;);
        System.out.println(&quot;Password: &quot; + PASSWORD + &quot;\n&quot;);

        // 先做一次 warm-up (JVM/JIT)
        warmUp();

        System.out.println(&quot;---- PBKDF2 (HmacSHA256) ----&quot;);
        pbkdf2HashAndVerify();

        System.out.println(&quot;---- bcrypt (jBCrypt) ----&quot;);
        bcryptHashAndVerify();

        System.out.println(&quot;---- scrypt (BouncyCastle) ----&quot;);
        scryptHashAndVerify();

        System.out.println(&quot;---- Argon2id (BouncyCastle) ----&quot;);
        argon2HashAndVerify();
    }

    private static void warmUp() {
        // 簡單 warmup 幾次，讓 JIT 編譯熱起來
        for (int i = 0; i &amp;lt; 3; i++) {
            try {
                pbkdf2Once();
                bcryptOnce();
                scryptOnce();
                argon2Once();
            } catch (Exception ignored) {}
        }
    }

    // ---------- PBKDF2 ----------
    private static void pbkdf2HashAndVerify() throws Exception {
        byte[] salt = new byte[16];
        RANDOM.nextBytes(salt);
        int iterations = 65536;
        int keyLen = 256; // bits

        long t0 = System.nanoTime();
        PBEKeySpec spec = new PBEKeySpec(PASSWORD.toCharArray(), salt, iterations, keyLen);
        SecretKeyFactory skf = SecretKeyFactory.getInstance(&quot;PBKDF2WithHmacSHA256&quot;);
        byte[] hash = skf.generateSecret(spec).getEncoded();
        long t1 = System.nanoTime();

        String stored = String.format(&quot;PBKDF2$%d$%s$%s&quot;,
                iterations,
                Base64.getEncoder().encodeToString(salt),
                Base64.getEncoder().encodeToString(hash));

        System.out.println(&quot;Stored: &quot; + stored);
        System.out.println(&quot;Hash time: &quot; + ((t1 - t0) / 1_000_000) + &quot; ms&quot;);

        // verify
        long v0 = System.nanoTime();
        boolean ok = verifyPBKDF2(PASSWORD, stored);
        long v1 = System.nanoTime();
        System.out.println(&quot;Verify: &quot; + ok + &quot; (time: &quot; + ((v1 - v0) / 1_000_000) + &quot; ms)\n&quot;);
    }

    private static boolean verifyPBKDF2(String password, String stored) throws Exception {
        // stored format: PBKDF2$iterations$base64salt$base64hash
        String[] parts = stored.split(&quot;\\$&quot;);
        int iterations = Integer.parseInt(parts[1]);
        byte[] salt = Base64.getDecoder().decode(parts[2]);
        byte[] expected = Base64.getDecoder().decode(parts[3]);

        PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, expected.length * 8);
        SecretKeyFactory skf = SecretKeyFactory.getInstance(&quot;PBKDF2WithHmacSHA256&quot;);
        byte[] computed = skf.generateSecret(spec).getEncoded();

        return MessageDigest.isEqual(computed, expected);
    }

    private static void pbkdf2Once() throws Exception {
        byte[] salt = new byte[8];
        RANDOM.nextBytes(salt);
        PBEKeySpec spec = new PBEKeySpec(PASSWORD.toCharArray(), salt, 1000, 128);
        SecretKeyFactory skf = SecretKeyFactory.getInstance(&quot;PBKDF2WithHmacSHA256&quot;);
        skf.generateSecret(spec).getEncoded();
    }

    // ---------- bcrypt ----------
    private static void bcryptHashAndVerify() {
        int cost = 12; // cost factor
        long t0 = System.nanoTime();
        String hash = BCrypt.hashpw(PASSWORD, BCrypt.gensalt(cost));
        long t1 = System.nanoTime();

        System.out.println(&quot;Stored: &quot; + hash);
        System.out.println(&quot;Hash time: &quot; + ((t1 - t0) / 1_000_000) + &quot; ms&quot;);

        long v0 = System.nanoTime();
        boolean ok = BCrypt.checkpw(PASSWORD, hash);
        long v1 = System.nanoTime();
        System.out.println(&quot;Verify: &quot; + ok + &quot; (time: &quot; + ((v1 - v0) / 1_000_000) + &quot; ms)\n&quot;);
    }

    private static void bcryptOnce() {
        BCrypt.hashpw(PASSWORD, BCrypt.gensalt(8));
    }

    // ---------- scrypt ----------
    private static void scryptHashAndVerify() {
        byte[] salt = new byte[16];
        RANDOM.nextBytes(salt);
        int N = 16384; // CPU/memory cost (2^14)
        int r = 8;
        int p = 1;
        int keyLen = 32;

        long t0 = System.nanoTime();
        byte[] derived = SCrypt.generate(PASSWORD.getBytes(StandardCharsets.UTF_8), salt, N, r, p, keyLen);
        long t1 = System.nanoTime();

        String stored = String.format(&quot;scrypt$%d$%d$%d$%s$%s&quot;,
                N, r, p,
                Base64.getEncoder().encodeToString(salt),
                Base64.getEncoder().encodeToString(derived));

        System.out.println(&quot;Stored: &quot; + stored);
        System.out.println(&quot;Hash time: &quot; + ((t1 - t0) / 1_000_000) + &quot; ms&quot;);

        long v0 = System.nanoTime();
        boolean ok = verifyScrypt(PASSWORD, stored);
        long v1 = System.nanoTime();
        System.out.println(&quot;Verify: &quot; + ok + &quot; (time: &quot; + ((v1 - v0) / 1_000_000) + &quot; ms)\n&quot;);
    }

    private static boolean verifyScrypt(String password, String stored) {
        // stored format: scrypt$N$r$p$base64salt$base64hash
        String[] parts = stored.split(&quot;\\$&quot;);
        int N = Integer.parseInt(parts[1]);
        int r = Integer.parseInt(parts[2]);
        int p = Integer.parseInt(parts[3]);
        byte[] salt = Base64.getDecoder().decode(parts[4]);
        byte[] expected = Base64.getDecoder().decode(parts[5]);

        byte[] computed = SCrypt.generate(password.getBytes(StandardCharsets.UTF_8), salt, N, r, p, expected.length);
        return MessageDigest.isEqual(computed, expected);
    }

    private static void scryptOnce() {
        byte[] salt = new byte[8];
        RANDOM.nextBytes(salt);
        SCrypt.generate(PASSWORD.getBytes(StandardCharsets.UTF_8), salt, 1024, 8, 1, 16);
    }

    // ---------- Argon2id ----------
    private static void argon2HashAndVerify() {
        byte[] salt = new byte[16];
        RANDOM.nextBytes(salt);
        int iterations = 3;
        int memoryKB = 64 * 1024; // 64 MB represented as KB here
        int parallelism = 1;
        int hashLen = 32;

        Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
                .withSalt(salt)
                .withParallelism(parallelism)
                .withMemoryAsKB(memoryKB)
                .withIterations(iterations)
                .build();

        Argon2BytesGenerator gen = new Argon2BytesGenerator();

        long t0 = System.nanoTime();
        gen.init(params);
        byte[] hash = new byte[hashLen];
        gen.generateBytes(PASSWORD.getBytes(StandardCharsets.UTF_8), hash);
        long t1 = System.nanoTime();

        String stored = String.format(&quot;argon2id$%d$%d$%d$%s$%s&quot;,
                iterations, memoryKB, parallelism,
                Base64.getEncoder().encodeToString(salt),
                Base64.getEncoder().encodeToString(hash));

        System.out.println(&quot;Stored: &quot; + stored);
        System.out.println(&quot;Hash time: &quot; + ((t1 - t0) / 1_000_000) + &quot; ms&quot;);

        long v0 = System.nanoTime();
        boolean ok = verifyArgon2(PASSWORD, stored);
        long v1 = System.nanoTime();
        System.out.println(&quot;Verify: &quot; + ok + &quot; (time: &quot; + ((v1 - v0) / 1_000_000) + &quot; ms)\n&quot;);
    }

    private static boolean verifyArgon2(String password, String stored) {
        // stored format: argon2id$iterations$memoryKB$parallelism$base64salt$base64hash
        String[] parts = stored.split(&quot;\\$&quot;);
        int iterations = Integer.parseInt(parts[1]);
        int memoryKB = Integer.parseInt(parts[2]);
        int parallelism = Integer.parseInt(parts[3]);
        byte[] salt = Base64.getDecoder().decode(parts[4]);
        byte[] expected = Base64.getDecoder().decode(parts[5]);

        Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
                .withSalt(salt)
                .withParallelism(parallelism)
                .withMemoryAsKB(memoryKB)
                .withIterations(iterations)
                .build();

        Argon2BytesGenerator gen = new Argon2BytesGenerator();
        gen.init(params);
        byte[] computed = new byte[expected.length];
        gen.generateBytes(password.getBytes(StandardCharsets.UTF_8), computed);

        return MessageDigest.isEqual(computed, expected);
    }

    private static void argon2Once() {
        byte[] salt = new byte[8];
        RANDOM.nextBytes(salt);
        Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
                .withSalt(salt)
                .withParallelism(1)
                .withMemoryAsKB(32)
                .withIterations(1)
                .build();
        Argon2BytesGenerator gen = new Argon2BytesGenerator();
        gen.init(params);
        byte[] out = new byte[16];
        gen.generateBytes(PASSWORD.getBytes(StandardCharsets.UTF_8), out);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;=== Hash + Verify Benchmark ===
Password: MyPassword123!

---- PBKDF2 (HmacSHA256) ----
Stored: PBKDF2$65536$uBq2UzPm/rU5/5QOOHgWwA==$0Ri1XVl8++OB5Pz7pXAXEAJ32M8i8degzp6PZPnyd5o=
Hash time: 61 ms
Verify: true (time: 52 ms)

---- bcrypt (jBCrypt) ----
Stored: $2a$12$FjXjsIfL7MQ8zz8oyNfx.O6sBoSkhG6hcMK/brtWvfFOlfFx0WD/a
Hash time: 215 ms
Verify: true (time: 213 ms)

---- scrypt (BouncyCastle) ----
Stored: scrypt$16384$8$1$C6G1/j3+24iT7CHfW8/Ifg==$HC7JX3pdmDTKk2tCMUdTQ/ezgyey/hLx2ZCAoePzdHs=
Hash time: 22 ms
Verify: true (time: 25 ms)

---- Argon2id (BouncyCastle) ----
Stored: argon2id$3$65536$1$6HpyoNfAlJQ32cFAmflEHQ==$vZ+hNK/DnNpj1Urbj0W76zT2nvm0qDV14HFMbW9O9J0=
Hash time: 136 ms
Verify: true (time: 133 ms)&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/181471462893373429/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/06/hash.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/181471462893373429'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/181471462893373429'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/06/hash.html' title='密碼 hash 演算法比較'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-1221335111204548329</id><published>2026-06-15T08:55:57.785+08:00</published><updated>2026-06-15T08:55:57.785+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>Argon2</title><content type='html'>&lt;p&gt;Argon2 是一種密碼雜湊 (password hashing) 演算法，也是 2015 年密碼雜湊競賽（PHC, Password Hashing Competition） 的獲勝者。它被設計來安全地儲存密碼，抵抗暴力破解和 GPU / ASIC 攻擊。&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;特性&quot;&gt;特性&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memory-hard&lt;/strong&gt;：需要大量記憶體計算，增加硬體破解成本。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Time-cost adjustable&lt;/strong&gt;：可設定運算次數（iterations），增加雜湊延遲。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Parallelism&lt;/strong&gt;：支援多執行緒並行計算。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;三種變體&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Argon2d&lt;/code&gt;：可抵抗 GPU 破解，記憶體存取取決於輸入密碼，但對側信道攻擊 side‑channel attack 敏感。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Argon2i&lt;/code&gt;：可抵抗側信道攻擊，使用固定存取模式，但稍慢。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Argon2id&lt;/code&gt;：混合模式，推薦用於大多數應用（兼顧 GPU 攻擊與側信道安全）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;側信道攻擊-sidechannel-attack&quot;&gt;側信道攻擊 side‑channel attack&lt;/h2&gt;
&lt;p&gt;側信道攻擊不是直接破解演算法數學弱點，而是利用系統在執行時「洩漏的額外資訊」來恢復密碼、金鑰。這些洩漏資訊稱為「側信道」，常見類型有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;時間（Timing）&lt;/strong&gt;：根據運算花費時間推斷內部邏輯或資料（例如不同輸入導致不同記憶體存取時間）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;快取/記憶體訪問模式（Cache / Memory access）&lt;/strong&gt;：透過觀察 CPU cache 命中/未命中或記憶體訪問順序推測密碼相關資料。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;電力（Power analysis）&lt;/strong&gt;：量測設備耗電曲線（特別在智慧卡、嵌入式裝置）來推斷內部運算。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;電磁輻射（EM）&lt;/strong&gt;：接收設備發出的電磁洩漏訊號。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;分支/投機執行漏洞（Spectre/Speculative exec）&lt;/strong&gt;：利用微架構行為取得不該看到的記憶體內容。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I/O 或錯誤回應差異&lt;/strong&gt;：例如錯誤訊息長短或序列差異可洩漏資訊。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;側信道攻擊常見條件：攻擊者和目標在&lt;strong&gt;同一台機器或共用硬體資源&lt;/strong&gt;（例如 cloud 共宿主、虛擬機、瀏覽器 JS 高解析計時）或在物理近距離（電力/EM）時更容易成功。遠端網路時延雜訊大，攻擊難度上升但並非不可能（需大量樣本與高解析度計時）。&lt;/p&gt;
&lt;p&gt;若攻擊者只有透過網路遠端（無共用硬體且無高解析計時），側信道攻擊較難。&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;三種變體&quot;&gt;三種變體&lt;/h2&gt;
&lt;p&gt;在記憶體存取模式上不同&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Argon2d&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;記憶體存取&lt;strong&gt;依賴輸入（data‑dependent）&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;優點：對 GPU / ASIC 破解更抗（memory‑hard），但&lt;strong&gt;容易洩漏記憶體訪問模式&lt;/strong&gt;，因此對於有本地或共宿主攻擊者（能觀察 cache/access pattern）的系統 &lt;strong&gt;較不安全&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Argon2i&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;記憶體存取&lt;strong&gt;獨立於輸入（data‑independent）&lt;/strong&gt;，以固定/預定方式存取記憶體。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;優點：&lt;strong&gt;較能抵抗側信道攻擊（尤其是記憶體訪問/快取型）&lt;/strong&gt;；缺點：通常較慢，對某些攻擊（GPU 的大量平行 brute force）耐性較弱。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Argon2id&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;混合模式：先用 Argon2i 的 data‑independent pass 再用 Argon2d 的 data‑dependent pass（或類似組合）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;建議：&lt;strong&gt;大多數情況使用 Argon2id&lt;/strong&gt; —— 在兼顧側信道與 GPU 抗性間取得平衡，是常見推薦選擇。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;java-example&quot;&gt;java example&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-xml&quot;&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.bouncycastle&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;bcprov-jdk18on&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.82&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Argon2BouncyCastleExample.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import org.bouncycastle.crypto.params.Argon2Parameters;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Arrays;

public class Argon2BouncyCastleExample {

    // 產生隨機 salt
    private static byte[] generateSalt(int length) {
        byte[] salt = new byte[length];
        new SecureRandom().nextBytes(salt);
        return salt;
    }

    // 將密碼 hash
    public static String hashPassword(String password, byte[] salt) {
        // 設定參數
        Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
                .withSalt(salt)
                .withIterations(3)       // 運算次數
                .withMemoryAsKB(65536)   // 記憶體使用量 KB (64 MB)
                .withParallelism(2);     // 併行線程數

        Argon2BytesGenerator generator = new Argon2BytesGenerator();
        generator.init(builder.build());

        byte[] hash = new byte[32]; // 32 bytes hash
        generator.generateBytes(password.toCharArray(), hash, 0, hash.length);

        // 回傳 base64 編碼
        return Base64.getEncoder().encodeToString(hash);
    }

    // 驗證密碼
    public static boolean verifyPassword(String password, String expectedHashBase64, byte[] salt) {
        String hashToCheck = hashPassword(password, salt);
        return Arrays.equals(Base64.getDecoder().decode(hashToCheck), Base64.getDecoder().decode(expectedHashBase64));
    }

    public static void main(String[] args) {
        String password = &quot;MyPassword123!&quot;;
        byte[] salt = generateSalt(16); // 16 bytes salt

        // hash
        String hash = hashPassword(password, salt);
        System.out.println(&quot;Salt (Base64): &quot; + Base64.getEncoder().encodeToString(salt));
        System.out.println(&quot;Hash (Base64): &quot; + hash);

        // 驗證
        boolean ok = verifyPassword(password, hash, salt);
        System.out.println(&quot;Password verified: &quot; + ok);

        // 測試錯誤密碼
        boolean fail = verifyPassword(&quot;wrongPassword&quot;, hash, salt);
        System.out.println(&quot;Wrong password verified: &quot; + fail);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;Salt (Base64): Uivew8iBmhiZaz7Wv2nSDw==
Hash (Base64): Ldm2DksAcnhUIHyJ4v4uzJJwLd5A2YIhTMn3277/tDU=
Password verified: true
Wrong password verified: false&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/zh-tw/Argon2&quot;&gt;Argon2 - 維基百科，自由的百科全書&lt;/a&gt;&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/1221335111204548329/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/06/argon2.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/1221335111204548329'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/1221335111204548329'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/06/argon2.html' title='Argon2'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-1704168875772204179</id><published>2026-06-01T08:33:54.162+08:00</published><updated>2026-06-01T08:33:54.162+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>scrypt</title><content type='html'>&lt;p&gt;scrypt 是一種password-based key derivation演算法。是加拿大計算機科學家暨計算機安全研究人員 Colin Percival 於2009年所發明的密鑰派生函數，當初設計用在他所創立的Tarsnap服務上。設計時考慮到大規模的客製硬體攻擊而刻意設計需要大量記憶體運算，可防止 GPU/ASIC 大規模破解。2016年，scrypt 演算法發佈在RFC 7914。scrypt的簡化版被用在數個密碼貨幣的工作量證明（Proof-of-Work）上。&lt;/p&gt;
&lt;p&gt;scrypt需要使用大量記憶體的原因來自於產生大量 pseudorandom 資料作為演算法計算的基礎。一旦這些資料被產生後，演算法將會以偽隨機性的順序讀取這些資料產生結果。因此最直接的實做方式將會需要大量記憶體將這些資料儲存在記憶體內供演算法計算。由於偽隨機性資料是透過演算法產生，在實作上也可以在需要存取時再計算以降低記憶體使用量。但由於計算成本很高，這個實作方法將大幅降低演算法的速度。&lt;/p&gt;
&lt;p&gt;主要的參數：&lt;/p&gt;
&lt;p&gt;透過 r, p 參數，調整記憶體使用量&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;參數&lt;/th&gt;
&lt;th&gt;說明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;迭代次數，耗費 CPU 與時間成本（必須是 2 的次方）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;r&lt;/td&gt;
&lt;td&gt;記憶體使用量（block size）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p&lt;/td&gt;
&lt;td&gt;並行度（parallelism）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;salt&lt;/td&gt;
&lt;td&gt;隨機值，避免彩虹表攻擊&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dkLen&lt;/td&gt;
&lt;td&gt;輸出 hash 長度，通常是 32 bytes / 64 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;java-sample&quot;&gt;java sample&lt;/h2&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-xml&quot;&gt;        &amp;lt;!-- scrypt --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.bouncycastle&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;bcprov-jdk18on&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.82&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ScryptBouncyCastleExample.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import org.bouncycastle.crypto.generators.SCrypt;
import java.security.SecureRandom;
import java.util.Base64;

public class ScryptBouncyCastleExample {

    // scrypt 參數
    private static final int N = 16384; // CPU cost
    private static final int r = 8;     // Memory cost
    private static final int p = 1;     // Parallelism
    private static final int KEY_LENGTH = 32;

    // 產生隨機 salt
    public static byte[] generateSalt() {
        byte[] salt = new byte[16];
        new SecureRandom().nextBytes(salt);
        return salt;
    }

    // Hash 密碼
    public static String hashPassword(String password, byte[] salt) {
        byte[] hash = SCrypt.generate(password.getBytes(), salt, N, r, p, KEY_LENGTH);
        return Base64.getEncoder().encodeToString(hash);
    }

    // 驗證密碼
    public static boolean verifyPassword(String password, byte[] salt, String storedHash) {
        String hashToVerify = hashPassword(password, salt);
        return hashToVerify.equals(storedHash);
    }

    public static void main(String[] args) {
        String password = &quot;MyPassword123!&quot;;

        // 產生 salt
        byte[] salt = generateSalt();

        // 生成 hash
        String hashed = hashPassword(password, salt);
        System.out.println(&quot;Salt (Base64): &quot; + Base64.getEncoder().encodeToString(salt));
        System.out.println(&quot;Hashed password: &quot; + hashed);

        // 驗證
        boolean match = verifyPassword(password, salt, hashed);
        System.out.println(&quot;Password match? &quot; + match);

        boolean wrong = verifyPassword(&quot;wrongPassword&quot;, salt, hashed);
        System.out.println(&quot;Password match with wrong password? &quot; + wrong);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;Salt (Base64): +VF7o5rmURY++GxWmKARhw==
Hashed password: PON5gVuqeiOM4MDszfY4DHBjBacpMdObu07WhdTaoyo=
Password match? true
Password match with wrong password? false&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/zh-tw/scrypt&quot;&gt;scrypt - 維基百科，自由的百科全書&lt;/a&gt;&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/1704168875772204179/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/06/scrypt.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/1704168875772204179'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/1704168875772204179'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/06/scrypt.html' title='scrypt'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-3968863106330649866</id><published>2026-05-25T08:31:50.024+08:00</published><updated>2026-05-25T08:31:50.024+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>PBKDF2</title><content type='html'>&lt;p&gt;PBKDF1 和 PBKDF2 是 Password-Based Key Derivation Function 1 and 2，具有可改變計算成本的金鑰衍生函式。PBKDF 是 RSA 的 PKCS 的一部分。PBKDF1 只能產生 160 bits 長度的金鑰。2000 年發布了 PBKDF2，也就是 PKCS #5 v2.0，以 RFC 2898 發布。2017 年發布了 PBKDF2 的更新版本，也就是 RFC 8018，PKCS #5 v2.1 版。&lt;/p&gt;
&lt;p&gt;PBKDF2 將 HMAC (hash-based message authentication code) 與 salt 一起套用於密碼加密，並多次重複這個過程，產生一個 derived key，增加了 CPU 運算，讓破解變得困難。&lt;/p&gt;
&lt;p&gt;2000 年的演算法建議最小疊代次數為 1000 次，隨著 CPU 效能提升，2005 年 Kerberos 標準建議改為 4096 次。apple 在 ios3 使用 3000 次，在 ios4 為 10000 次。2023 年，OWASP 建議PBKDF2-HMAC-SHA256使用600,000次迭代，PBKDF2-HMAC-SHA512使用210,000 次迭代。&lt;/p&gt;
&lt;p&gt;PBKDF2 雖然可以通過改變迭代次數來任意調整所需的計算時間，但它可以用一個小電路和很少的RAM來實現，透過使用特殊應用積體電路（ASIC）或圖形處理器（GPU）進行暴力攻擊會比較容易。&lt;/p&gt;
&lt;p&gt;Bcrypt 密碼雜湊函式需要更大的RAM（但仍然不能單獨調整，由給定的CPU時間決定）並且對此類攻擊的抵抗力稍強，更現代的Scrypt金鑰衍生函式可以任意使用大量主記憶體，因此更能抵抗ASIC和GPU攻擊。&lt;/p&gt;
&lt;p&gt;2013 年舉辦的 Password Hashing Competition (PHC) 鼓勵開發更好的密碼 hash 演算法。於 2015/7/20，Argon2 被選為最終的獲勝者。&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;java-sample&quot;&gt;java sample&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.util.Base64;

public class PBKDF2Example {

    private static final int SALT_LENGTH = 16;      // 16 bytes
    private static final int ITERATIONS = 65536;    // iteration count
    private static final int KEY_LENGTH = 256;      // 256-bit

    // 產生隨機 salt
    public static byte[] generateSalt() {
        SecureRandom sr = new SecureRandom();
        byte[] salt = new byte[SALT_LENGTH];
        sr.nextBytes(salt);
        return salt;
    }

    // 將密碼 + salt 轉為 PBKDF2 hash
    public static byte[] hashPassword(char[] password, byte[] salt) {
        try {
            PBEKeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH);
            SecretKeyFactory skf = SecretKeyFactory.getInstance(&quot;PBKDF2WithHmacSHA256&quot;);
            return skf.generateSecret(spec).getEncoded();
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new RuntimeException(&quot;Error while hashing password&quot;, e);
        }
    }

    // 驗證密碼
    public static boolean verifyPassword(char[] password, byte[] salt, byte[] expectedHash) {
        byte[] pwdHash = hashPassword(password, salt);
        if (pwdHash.length != expectedHash.length) return false;
        for (int i = 0; i &amp;lt; pwdHash.length; i++) {
            if (pwdHash[i] != expectedHash[i]) return false;
        }
        return true;
    }

    public static void main(String[] args) {
        String password = &quot;MyPassword123!&quot;;

        // 生成 salt
        byte[] salt = generateSalt();

        // 生成 hash
        byte[] hash = hashPassword(password.toCharArray(), salt);

        System.out.println(&quot;Salt: &quot; + Base64.getEncoder().encodeToString(salt));
        System.out.println(&quot;PBKDF2 Hash: &quot; + Base64.getEncoder().encodeToString(hash));

        // 驗證正確密碼
        boolean valid = verifyPassword(password.toCharArray(), salt, hash);
        System.out.println(&quot;Password matched? &quot; + valid);

        // 驗證錯誤密碼
        boolean invalid = verifyPassword(&quot;wrongPassword&quot;.toCharArray(), salt, hash);
        System.out.println(&quot;Wrong password verified? &quot; + invalid);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;Salt: aDRyAM6YJD1p8W0SN8U7pg==
PBKDF2 Hash: 2lXNtraA2JIUdSNL5xcj/seHjOpIuTxkarTxKItRabw=
Password matched? true
Wrong password verified? false&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/zh-tw/PBKDF2&quot;&gt;PBKDF2 - 維基百科，自由的百科全書&lt;/a&gt;&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/3968863106330649866/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/05/pbkdf2.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/3968863106330649866'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/3968863106330649866'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/05/pbkdf2.html' title='PBKDF2'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-8786570717372795947</id><published>2026-05-18T08:32:25.549+08:00</published><updated>2026-05-18T08:32:25.550+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>bcrypt</title><content type='html'>&lt;p&gt;bcrypt 是一種專為密碼儲存設計的單向雜湊演算法（hashing algorithm）。 由美國電腦科學家 Niels Provos 及 David Mazières 根據 Blowfish 加密演算法所設計的密碼雜湊函式，於1999年在USENIX中展示。並加入了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Salt — 防止字典攻擊與 rainbow table 攻擊&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;工作因子（cost factor） — 運算次數 = &lt;code&gt;2^cost&lt;/code&gt;，控制計算複雜度，讓暴力破解更慢&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;大約耗時（現代CPU）&lt;/th&gt;
&lt;th&gt;用途建議&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;約 50ms&lt;/td&gt;
&lt;td&gt;測試環境&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;約 100–200ms&lt;/td&gt;
&lt;td&gt;一般 Web 登入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;約 300–500ms&lt;/td&gt;
&lt;td&gt;高安全性需求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14+&lt;/td&gt;
&lt;td&gt;&amp;gt;1 秒&lt;/td&gt;
&lt;td&gt;金融級或低頻操作&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;bcrypt-運作流程&quot;&gt;bcrypt 運作流程&lt;/h2&gt;
&lt;p&gt;注意密碼最多只支援 72 bytes&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;輸入：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;明文密碼（例如 &quot;MyPassword123!&quot;），最多 72 bytes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;工作因子（cost，例如 10）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;p&gt;產生隨機 Salt（16 bytes）， 是一個 16 bytes(128 bits) salt value&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;進行多輪 Blowfish 加密運算，運算次數 = 2^cost
ex: cost=10 → 1024 次加密循環
cost=12 → 4096 次循環&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;輸出：產生一個 60 字元的 hash 字串，裡面包含版本, cost factor, salt及 hash 結果&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;bcrypt-格式&quot;&gt;bcrypt 格式&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;#格式
$2&amp;lt;a/b/x/y&amp;gt;$[cost]$[22 character salt][31 character hash]
# sample
$2a$10$E6h8dSmDsC8Kz.bJb3SuPO0h9msLniD9pD8bZjq4nLZdTIm.CDzMy
$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;格式說明&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;$2a$    → bcrypt 版本  
10$     → cost factor  
E6h8dSmDsC8Kz.bJb3SuPO  → Base64 編碼的 salt  
0h9msLniD9pD8bZjq4nLZdTIm.CDzMy  → hash 結果&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;$2a$    → bcrypt 版本  
12$     → cost factor  
R9h/cIPz0gi.URNNX3kh2O  → Base64 編碼的 salt  
PST9/PgBkqquzi.Ss7KIUgO2t0jWMUW  → hash 結果&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;version&quot;&gt;Version&lt;/h2&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;dollar2dollar-1999&quot;&gt;&lt;code&gt;$2$ (1999)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;最初始的 bcrypt 版本&lt;/p&gt;
&lt;p&gt;在 OpenBSD 密碼檔案裡面用以下幾種方式定義使用哪一種加密方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$1$&lt;/code&gt;: MD5-based crypt (&#39;md5crypt&#39;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$2$&lt;/code&gt;: Blowfish-based crypt (&#39;bcrypt&#39;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$sha1$&lt;/code&gt;: SHA-1-based crypt (&#39;sha1crypt&#39;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$5$&lt;/code&gt;: SHA-256-based crypt (&#39;sha256crypt&#39;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$6$&lt;/code&gt;: SHA-512-based crypt (&#39;sha512crypt&#39;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;dollar2adollar&quot;&gt;&lt;code&gt;$2a$&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;初始版本的 bcrypt，沒有定義如何處理 non-ASCII 字元，也沒有處理 null terminator。這個版本的 bcrypt 限制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;字串必須要用UTF-8 encoding&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;必須要包含 null terminator&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;dollar2xdollar-dollar2ydollar-june-2011&quot;&gt;&lt;code&gt;$2x$, $2y$ (June 2011)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;因 2011/6 在 php 版本的 crypt_blowfish 發現了一個 bug，在 8th bit 設定時，會處理錯誤。故建議把 &lt;code&gt;$2a$&lt;/code&gt;更新為 &lt;code&gt;$2x$&lt;/code&gt; ，同時做了 &lt;code&gt;$2y$&lt;/code&gt;。但 2x/2y 都沒有被廣泛採用&lt;/p&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;dollar2bdollar-february-2014&quot;&gt;&lt;code&gt;$2b$ (February 2014)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;OpenBSD 版本的 bcrypt 發現了一個 bug，是使用 unsigned 8-bit 數值儲存密碼長度。超過 255 bytes 的密碼，會以少於 72 bytes 的長度切割。 ex: 260 bytes 被以 4 bytes 切割。&lt;/p&gt;
&lt;p&gt;OpenBSD 修正這個問題，並改版為 &lt;code&gt;$2b$&lt;/code&gt;&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;java-sample&quot;&gt;Java Sample&lt;/h2&gt;
&lt;p&gt;有兩個 java library，org.mindrot.jbcrypt 版本比較舊，沒有支援新版的演算法，且已經沒有在維護。at.favre.lib.crypto.bcrypt 有支援新的演算法，且有支援超過 72 bytes 密碼的處理方式。&lt;/p&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-xml&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;
         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;groupId&amp;gt;com.maxkit.test&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;test&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0&amp;lt;/version&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;17&amp;lt;/maven.compiler.source&amp;gt;
        &amp;lt;maven.compiler.target&amp;gt;17&amp;lt;/maven.compiler.target&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.mindrot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;jbcrypt&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;0.4&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;at.favre.lib&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;bcrypt&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;0.10.2&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&amp;lt;/project&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;mindrotbcryptjava&quot;&gt;MindrotBcrypt.java&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import org.mindrot.jbcrypt.BCrypt;

public class MindrotBcrypt {
    public static void main(String[] args) {
        String password = &quot;MyPassword123!&quot;;

        // cost factor (越大越慢，預設常見是 10~12)
        int cost = 12;

        // 產生 salt
        String salt = BCrypt.gensalt(cost);
        System.out.println(&quot;Salt: &quot; + salt);

        // Hash 密碼
        String hashed = BCrypt.hashpw(password, salt);
        System.out.println(&quot;BCrypt Hash: &quot; + hashed);

        // 驗證密碼
        boolean matched = BCrypt.checkpw(password, hashed);
        System.out.println(&quot;Password matched? &quot; + matched);

        // 測試錯誤密碼
        boolean matchedWrong = BCrypt.checkpw(&quot;WrongPassword&quot;, hashed);
        System.out.println(&quot;Wrong password verified? &quot; + matchedWrong);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;Salt: $2a$12$.ZzlOaYpDmUSvFCq/FJaM.
BCrypt Hash: $2a$12$.ZzlOaYpDmUSvFCq/FJaM.lEUlNjTrXH8JuUAN96fq80yiKSyV0Q.
Password matched? true
Wrong password verified? false&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;bcryptfavrebcryptjava&quot;&gt;BcryptFavreBcrypt.java&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import at.favre.lib.crypto.bcrypt.BCrypt;

public class BcryptFavreBcrypt {
    public static void main(String[] args) {
        String password = &quot;MyPassword123!&quot;;
        int cost = 12;

        // 產生 hash
        String bcryptHashString = BCrypt.withDefaults().hashToString(cost, password.toCharArray());
        System.out.println(&quot;Hash: &quot; + bcryptHashString);

        // 驗證密碼
        BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), bcryptHashString);
        if (result.verified) {
            System.out.println(&quot;Password verified OK&quot;);
        } else {
            System.out.println(&quot;Password verification failed&quot;);
        }

        // 錯誤密碼測試
        BCrypt.Result resultWrong = BCrypt.verifyer().verify(&quot;WrongPassword&quot;.toCharArray(), bcryptHashString);
        System.out.println(&quot;Wrong password verified? &quot; + resultWrong.verified);

        //////
        // 產生 2y hash
        String bcryptHashString2Y = BCrypt.with(BCrypt.Version.VERSION_2Y).hashToString(cost, password.toCharArray());
        System.out.println(&quot;2Y Hash: &quot; + bcryptHashString2Y);
        // 驗證密碼
        BCrypt.Result result2Y = BCrypt.verifyer().verify(password.toCharArray(), bcryptHashString2Y);
        if (result2Y.verified) {
            System.out.println(&quot;2Y Password verified OK&quot;);
        } else {
            System.out.println(&quot;2Y Password verification failed&quot;);
        }

        // 產生 2b hash
        String bcryptHashString2B = BCrypt.with(BCrypt.Version.VERSION_2Y).hashToString(cost, password.toCharArray());
        System.out.println(&quot;2B Hash: &quot; + bcryptHashString2B);
        // 驗證密碼
        BCrypt.Result result2B = BCrypt.verifyer().verify(password.toCharArray(), bcryptHashString2B);
        if (result2B.verified) {
            System.out.println(&quot;2B Password verified OK&quot;);
        } else {
            System.out.println(&quot;2B Password verification failed&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;Hash: $2a$12$BUc45rHJsnLWDkFW8CJN4.5QB.KUSg2ofIcejoeJ24OTmznz8BD8a
Password verified OK
Wrong password verified? false
2Y Hash: $2y$12$vRH74gxo2LZN/wbFj4oKdO6RrGgOgWTHxrhw.OUoMt9ZT1WqPT8Uq
2Y Password verified OK
2B Hash: $2y$12$7y.j7ocPnDqcbspURCddzeTn25HWgtQR1LHy20Wo./N81ygBd7m92
2B Password verified OK&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/Bcrypt&quot;&gt;bcrypt - 維基百科，自由的百科全書&lt;/a&gt;&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/8786570717372795947/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/05/bcrypt.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/8786570717372795947'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/8786570717372795947'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/05/bcrypt.html' title='bcrypt'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-6224041376731567745</id><published>2026-05-04T08:42:21.642+08:00</published><updated>2026-05-04T08:42:57.187+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>Java Atomic Variables</title><content type='html'>&lt;p&gt;java.util.concurrent.atomic 定義了對單一變數進行 atomic operations 的類別，所有類別都有 get 與 set methods，可讀寫 volatile variables。set 可確定會在 get 的前面先處理。&lt;/p&gt;
&lt;p&gt;對於物件管理，如果有兩個 thread 同時做 get/set，會導致資料異常，所以通常會在 set method 加上 synchronized 來限制一次只有一個 thread 進入該 method&lt;/p&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;p&gt;Counter 類別的變數 c，會因為多個 thread 同時使用而異常&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以會改寫為&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;synchronized 可解決 multi-thread 問題，但會產生效能問題。&lt;/p&gt;
&lt;p&gt;以 AtomicInteger 改寫&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;atomic-operation&quot;&gt;Atomic Operation&lt;/h2&gt;
&lt;p&gt;non-blocking 演算法提供了 compare-and-swap CAS 方法，可確保資料完整性。&lt;/p&gt;
&lt;p&gt;CAS operation 使用以下三個 operands&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;M 要操作 operate 的記憶體位置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A 該變數期待的既有原始數值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;B 需要被設定的新數值&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;CAS operation 可自動 atomically 從 M 到 B 更新數值，只會在 M 確認為 A 時，更新為 B&lt;/p&gt;
&lt;p&gt;因此可不使用 synchronization lock，完成資料的更新，沒有 thread 會被 suspended，也不需要 context switching。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;常用的 Atomic Variables 有 AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference，分別代表 int, long, boolean, object reference 這些數值可自動被安全地更新，主要 methods 有&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;get()&lt;/p&gt;
&lt;p&gt;等同於讀取 volatile variable，可直接取得其他 thread 更新的數值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;incrementAndGet()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;set()&lt;/p&gt;
&lt;p&gt;等同於寫入 volatile variable&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;lazySet()&lt;/p&gt;
&lt;p&gt;最終會寫入變數，但不保證可立即被其他 thread 取得。速度比 set 更快。可用在 cache，或被 gc 的資料&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;compareAndSet()&lt;/p&gt;
&lt;p&gt;成功就回傳 true&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;weakCompareAndSetPlain(), weakCompareAndSetVolatile()&lt;/p&gt;
&lt;p&gt;weakCompareAndSet() 已被 deprecated&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/essential/concurrency/atomicvars.html&quot;&gt;# Atomic Variables Java Tutorial&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.baeldung.com/java-atomic-variables&quot;&gt;An Introduction to Atomic Variables in Java | Baeldung&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/atomic/package-summary.html&quot;&gt;java.util.concurrent.atomic (Java SE 25 &amp;amp; JDK 25)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/java/atomic-variables-in-java-with-examples/&quot;&gt;Atomic Variables in Java with Examples - GeeksforGeeks&lt;/a&gt;&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/6224041376731567745/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/05/java-atomic-variables.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/6224041376731567745'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/6224041376731567745'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/05/java-atomic-variables.html' title='Java Atomic Variables'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-34466994667554533</id><published>2026-04-27T08:40:33.512+08:00</published><updated>2026-04-27T08:40:55.045+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>FFMForeign Function and Memory (FFM) API</title><content type='html'>&lt;p&gt;為了使用外部函式庫，java 需要跟原生的 native code 進行互動，傳統是使用 Java Native Interface JNI 進行開發，但 JNI 的開發過程繁複，需要定義 native method，然後從 Java Source Code 生成 C 的 header file，再用C 語言實作，然後才能編譯並連結原生開發的函式庫。&lt;/p&gt;
&lt;p&gt;Foreign Function &amp;amp; Memory API（FFM API）參考了其他語言的方法，實作外部函式庫的 Interface，提供更安全有效率的方法，存取本地端的記憶體及函式，取代了 JNI 的功能。這是自 JDK 22 開始的一組新的 API，用來取代 JNI。能夠呼叫 C/C++/Rust 的非 Java 原生函式庫，可直接操作記憶體。&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;jni-sample&quot;&gt;JNI sample&lt;/h2&gt;
&lt;p&gt;JNIDemo.java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;public class JNIDemo {
    static {
        System.loadLibrary(&quot;JNIDemo&quot;);
    }

    public static void main(String[] args) {
        new JNIDemo().printHelloWorld();
    }

    private native void printHelloWorld();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;這裡面，有一個 &lt;code&gt;private native void printHelloWorld()&lt;/code&gt;，這就是還沒有實作的原生函式。&lt;/p&gt;
&lt;p&gt;用以下指令，編譯 class 並產生 header file&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;javac -h . JNIDemo.java&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JNIDemo.h&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-c&quot;&gt;/* DO NOT EDIT THIS FILE - it is machine generated */
#include &amp;lt;jni.h&amp;gt;
/* Header for class JNIDemo */

#ifndef _Included_JNIDemo
#define _Included_JNIDemo
#ifdef __cplusplus
extern &quot;C&quot; {
#endif
/*
 * Class:     JNIDemo
 * Method:    printHelloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JNIDemo_printHelloWorld
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;編譯 C library&lt;/p&gt;
&lt;p&gt;因為在 macos 先查詢 JAVA_HOME 目錄&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;$ /usr/libexec/java_home
/opt/local/Library/Java/JavaVirtualMachines/jdk-25-azul-zulu.jdk/Contents/Home&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;編譯&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;JAVA_HOME=/opt/local/Library/Java/JavaVirtualMachines/jdk-25-azul-zulu.jdk/Contents/Home
gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin -dynamiclib JNIDemo.c -o libJNIDemo.dylib&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;會產生 libJNIDemo.dylib&lt;/p&gt;
&lt;p&gt;執行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;$ java -cp . -Djava.library.path=. JNIDemo
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::loadLibrary has been called by JNIDemo in an unnamed module (file:/Users/charley/Downloads/)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

Hello World!&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;ffm&quot;&gt;FFM&lt;/h2&gt;
&lt;p&gt;FFM API 能夠讓 Java 程式在不使用 JNI 的情況下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;更安全地存取記憶體&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;直接呼叫 C function&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;呼叫 C/系統的 library&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FFM 核心元件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Linker：對應 C 語言呼叫約定的連結器，用來呼叫 native 函式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SymbolLookup: 在 native library 或 process 中查找符號（例如 &lt;code&gt;printf&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;MemorySegment: 表示一塊連續的記憶體（on-heap 或 off-heap）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Arena：管理 &lt;code&gt;MemorySegment&lt;/code&gt; 的生命週期（自動釋放資源）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ValueLayout：描述原生型別的記憶體布局，例如 &lt;code&gt;JAVA_INT&lt;/code&gt;, &lt;code&gt;C_DOUBLE&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;FunctionDescriptor：描述 native 函式的簽章（參數與回傳型別）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;測試1&quot;&gt;測試1&lt;/h2&gt;
&lt;p&gt;測試呼叫 strlen&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.nio.charset.StandardCharsets;

public class FFMStrlenDemo {
    public static void main(String[] args) throws Throwable {
        // 1. 取得系統的 Linker（如 SystemV ABI 或 Windows ABI）
        Linker linker = Linker.nativeLinker();

        // 2. 找出 symbol (在 libc 裡)
        SymbolLookup stdlib = Linker.nativeLinker().defaultLookup();
        MemorySegment strlenAddr = stdlib.find(&quot;strlen&quot;).orElseThrow();

        // 3. 建立 Java 對應的 MethodHandle
        MethodHandle strlen = linker.downcallHandle(
            strlenAddr,
            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
        );

        // 4. 建立要傳入的字串記憶體
        // try (Arena arena = Arena.ofConfined()) {
        //     MemorySegment str = arena.allocateUtf8String(&quot;Hello FFM API!&quot;);
        //     long len = (long) strlen.invoke(str);
        //     System.out.println(&quot;Length = &quot; + len);
        // }
        try (Arena arena = Arena.ofConfined()) {
            // 手動建立 UTF-8 null-terminated string
            byte[] bytes = &quot;Hello FFM API!&quot;.getBytes(StandardCharsets.UTF_8);
            MemorySegment str = arena.allocate(bytes.length + 1, 1);
            str.asSlice(0, bytes.length).copyFrom(MemorySegment.ofArray(bytes));
            str.set(ValueLayout.JAVA_BYTE, bytes.length, (byte) 0); // &#39;\0&#39;

            long len = (long) strlen.invoke(str);
            System.out.println(&quot;Length = &quot; + len);
        }

        // allocate set/get 測試
        try (Arena arena = Arena.ofConfined()) {
            MemorySegment seg = arena.allocate(ValueLayout.JAVA_INT);
            seg.set(ValueLayout.JAVA_INT, 0, 42); // 寫入 42
            int value = seg.get(ValueLayout.JAVA_INT, 0);
            System.out.println(&quot;Read value: &quot; + value);
        } // arena 自動釋放記憶體
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;檢查 allocate API&lt;/p&gt;
&lt;p&gt;標準 API 是 &lt;code&gt;java.lang.foreign.Arena.allocateUtf8String(String)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;但測試，結果是 allocate&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# javap -classpath $JAVA_HOME/lib/modules java.lang.foreign.Arena | grep allocate

public abstract java.lang.foreign.MemorySegment allocate(long, long);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;這表示目前這個 openjdk 還是使用舊版的 allocate，缺少 allocateUtf8String&lt;/p&gt;
&lt;p&gt;編譯，執行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;javac FFMStrlenDemo.java
java --enable-native-access=ALL-UNNAMED FFMStrlenDemo&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;Length = 14
Read value: 42&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;測試2&quot;&gt;測試2&lt;/h2&gt;
&lt;p&gt;sum.c&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-c&quot;&gt;// sum.c
#include &amp;lt;stdio.h&amp;gt;

int sum(int a, int b) {
    return a + b;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;編譯為 library&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;## linux
# gcc -shared -fPIC -o libsum.so sum.c

## macos
gcc -shared -fPIC -o libsum.dylib sum.c&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SumFFMDemo.java 放在同一個目錄&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

public class SumFFMDemo {
    public static void main(String[] args) throws Throwable {
        // 載入我們的動態函式庫
        System.loadLibrary(&quot;sum&quot;); // 對應 libsum.so / sum.dll

        // 建立 linker
        Linker linker = Linker.nativeLinker();

        // 查找 symbol
        SymbolLookup lookup = SymbolLookup.libraryLookup(&quot;libsum.dylib&quot;, Arena.global());
        MemorySegment funcAddr = lookup.find(&quot;sum&quot;).orElseThrow();

        // 建立函式描述: int (int, int)
        FunctionDescriptor fd = FunctionDescriptor.of(
                ValueLayout.JAVA_INT, // return type
                ValueLayout.JAVA_INT, // param a
                ValueLayout.JAVA_INT  // param b
        );

        // 建立 method handle
        MethodHandle sum = linker.downcallHandle(funcAddr, fd);

        // 呼叫 native 函式
        int result = (int) sum.invoke(12, 30);
        System.out.println(&quot;sum(12, 30) = &quot; + result);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;編譯&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;javac SumFFMDemo.java
java --enable-native-access=ALL-UNNAMED SumFFMDemo
# java -Djava.library.path=. --enable-native-access=ALL-UNNAMED SumFFMDemo&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;sum(12, 30) = 42&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/34466994667554533/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/04/ffmforeign-function-and-memory-ffm-api.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/34466994667554533'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/34466994667554533'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/04/ffmforeign-function-and-memory-ffm-api.html' title='FFMForeign Function and Memory (FFM) API'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-1678501267278289169</id><published>2026-04-20T08:30:14.521+08:00</published><updated>2026-04-20T08:30:44.354+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>Compact Object Header</title><content type='html'>&lt;p&gt;在 JDK 25 中，&lt;strong&gt;Compact Object Headers&lt;/strong&gt;（緊湊型物件標頭）已經正式成為 HotSpot JVM 的預設功能，並且不再需要使用 &lt;code&gt;-XX:+UseCompactObjectHeaders&lt;/code&gt; 啟用這個功能。這是 JEP 519 的一部分，用途是進一步優化 Java 物件的記憶體佈局。在 JDK 24 中，這個功能曾作為實驗性功能引入，在 JDK 25 中轉為正式功能。&lt;/p&gt;
&lt;p&gt;在 64 位架構的 HotSpot JVM 中，物件標頭的大小從原本的 12 至 16 字節（取決於 JVM 配置）縮減至 8 字節（64 位）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;減少記憶體佔用&lt;/strong&gt;：物件標頭變小，整體記憶體佔用降低。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;提高快取效率&lt;/strong&gt;：更緊湊的記憶體佈局有助於提升 CPU 快取的命中率。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;降低垃圾回收壓力&lt;/strong&gt;：減少記憶體佔用量，有助於減少垃圾回收的次數。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;提升部署密度&lt;/strong&gt;：在容器化環境中，減少記憶體佔用量有助於提高部署密度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Project Lilliput 的目標是將物件標頭的大小進一步縮小至 4 字節。然而，這樣的改變需要更深入的研究和測試，以確保不會影響 JVM 的穩定性和性能。&lt;/p&gt;
&lt;p&gt;測試&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryUsage;
import java.util.ArrayList;
import java.util.List;

/**
 * JDK 25 Compact Object Header 功能測試
 * 
 * 功能說明：
 * Compact Object Header (JEP 450) 是 JDK 25 引入的實驗性特性
 * 目的是減少物件頭的記憶體開銷，從傳統的 12-16 bytes 減少到 8 bytes
 * 
 * 啟用方式：
 * java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders YourClass
 * 
 * 主要優勢：
 * 1. 減少記憶體佔用（每個物件節省 4-8 bytes）
 * 2. 提升快取效率（更好的資料局部性）
 * 3. 適合大量小物件的應用場景
 */
public class CompactObjectHeaderTest {

    private static final int OBJECT_COUNT = 1_000_000;

    static class SmallObject {
        private int id;
        private String name;

        public SmallObject(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println(&quot;=== JDK 25 Compact Object Header 測試 ===\n&quot;);

        // 檢查 JVM 參數
        checkJVMFlags();

        // 顯示初始記憶體狀態
        System.out.println(&quot;\n--- 初始記憶體狀態 ---&quot;);
        printMemoryUsage();

        // 建立大量物件並測試
        System.out.println(&quot;\n--- 建立 &quot; + OBJECT_COUNT + &quot; 個物件 ---&quot;);
        List&amp;lt;SmallObject&amp;gt; objects = new ArrayList&amp;lt;&amp;gt;(OBJECT_COUNT);

        long startTime = System.currentTimeMillis();
        long startMemory = getUsedMemory();

        for (int i = 0; i &amp;lt; OBJECT_COUNT; i++) {
            objects.add(new SmallObject(i, &quot;Object_&quot; + i));
        }

        long endTime = System.currentTimeMillis();
        long endMemory = getUsedMemory();

        // 強制垃圾回收以獲得更準確的記憶體使用量
        System.gc();
        Thread.sleep(100);
        long afterGCMemory = getUsedMemory();

        // 顯示測試結果
        System.out.println(&quot;\n--- 測試結果 ---&quot;);
        System.out.println(&quot;建立時間: &quot; + (endTime - startTime) + &quot; ms&quot;);
        System.out.println(&quot;記憶體增量 (建立後): &quot; + formatBytes(endMemory - startMemory));
        System.out.println(&quot;記憶體增量 (GC後): &quot; + formatBytes(afterGCMemory - startMemory));
        System.out.println(&quot;平均每個物件: &quot; + formatBytes((afterGCMemory - startMemory) / OBJECT_COUNT));

        // 顯示最終記憶體狀態
        System.out.println(&quot;\n--- 最終記憶體狀態 ---&quot;);
        printMemoryUsage();

        // 物件頭大小估算
        System.out.println(&quot;\n--- 物件頭分析 ---&quot;);
        analyzeObjectHeader(afterGCMemory - startMemory);

        // 保持物件存活
        System.out.println(&quot;\n物件數量: &quot; + objects.size());
        System.out.println(&quot;\n提示: 使用 -XX:+UseCompactObjectHeaders 啟用壓縮物件頭&quot;);
        System.out.println(&quot;比較指令: java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders CompactObjectHeaderTest&quot;);
    }

    private static void checkJVMFlags() {
        System.out.println(&quot;JVM 版本: &quot; + System.getProperty(&quot;java.version&quot;));
        System.out.println(&quot;JVM 供應商: &quot; + System.getProperty(&quot;java.vendor&quot;));

        List&amp;lt;String&amp;gt; inputArgs = ManagementFactory.getRuntimeMXBean().getInputArguments();
        System.out.println(&quot;\nJVM 參數:&quot;);
        for (String arg : inputArgs) {
            System.out.println(&quot;  &quot; + arg);
        }

        boolean hasCompactHeadersFlag = inputArgs.stream()
            .anyMatch(arg -&amp;gt; arg.contains(&quot;UseCompactObjectHeaders&quot;));
        boolean isEnabled = inputArgs.stream()
            .anyMatch(arg -&amp;gt; arg.contains(&quot;+UseCompactObjectHeaders&quot;));
        boolean isDisabled = inputArgs.stream()
            .anyMatch(arg -&amp;gt; arg.contains(&quot;-UseCompactObjectHeaders&quot;));

        if (isEnabled) {
            System.out.println(&quot;\n✓ Compact Object Headers 已明確啟用 (+UseCompactObjectHeaders)&quot;);
        } else if (isDisabled) {
            System.out.println(&quot;\n✗ Compact Object Headers 已明確禁用 (-UseCompactObjectHeaders)&quot;);
        } else if (hasCompactHeadersFlag) {
            System.out.println(&quot;\n? Compact Object Headers 參數已設定&quot;);
        } else {
            System.out.println(&quot;\n- Compact Object Headers 使用預設設定&quot;);
        }
    }

    private static void printMemoryUsage() {
        Runtime runtime = Runtime.getRuntime();
        long totalMemory = runtime.totalMemory();
        long freeMemory = runtime.freeMemory();
        long usedMemory = totalMemory - freeMemory;
        long maxMemory = runtime.maxMemory();

        System.out.println(&quot;已使用記憶體: &quot; + formatBytes(usedMemory));
        System.out.println(&quot;總分配記憶體: &quot; + formatBytes(totalMemory));
        System.out.println(&quot;最大可用記憶體: &quot; + formatBytes(maxMemory));
        System.out.println(&quot;可用記憶體: &quot; + formatBytes(freeMemory));
    }

    private static long getUsedMemory() {
        Runtime runtime = Runtime.getRuntime();
        return runtime.totalMemory() - runtime.freeMemory();
    }

    private static String formatBytes(long bytes) {
        if (bytes &amp;lt; 1024) return bytes + &quot; B&quot;;
        if (bytes &amp;lt; 1024 * 1024) return String.format(&quot;%.2f KB&quot;, bytes / 1024.0);
        if (bytes &amp;lt; 1024 * 1024 * 1024) return String.format(&quot;%.2f MB&quot;, bytes / (1024.0 * 1024));
        return String.format(&quot;%.2f GB&quot;, bytes / (1024.0 * 1024 * 1024));
    }

    private static void analyzeObjectHeader(long totalMemory) {
        // 估算物件頭大小
        // 傳統物件頭: 12 bytes (32-bit) 或 16 bytes (64-bit 壓縮指標)
        // Compact 物件頭: 8 bytes

        long avgBytesPerObject = totalMemory / OBJECT_COUNT;

        System.out.println(&quot;平均每個物件記憶體: &quot; + avgBytesPerObject + &quot; bytes&quot;);
        System.out.println(&quot;\n理論估算:&quot;);
        System.out.println(&quot;  - 傳統物件頭: 16 bytes (mark word + klass pointer)&quot;);
        System.out.println(&quot;  - Compact 物件頭: 8 bytes&quot;);
        System.out.println(&quot;  - int 欄位: 4 bytes&quot;);
        System.out.println(&quot;  - String 參考: 4-8 bytes (壓縮指標)&quot;);
        System.out.println(&quot;  - 對齊填充: 可能需要額外空間&quot;);

        if (avgBytesPerObject &amp;lt;= 70) {
            System.out.println(&quot;\n✓ 可能正在使用 Compact Object Headers&quot;);
        } else {
            System.out.println(&quot;\n✗ 可能使用傳統物件頭&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;編譯後，用這兩種方式執行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# 禁用 Compact Object Headers
java -XX:-UseCompactObjectHeaders -Xms512m -Xmx512m CompactObjectHeaderTest &amp;gt; test_without_compact.log 2&amp;gt;&amp;amp;1
# 啟用
java -XX:+UseCompactObjectHeaders -Xms512m -Xmx512m CompactObjectHeaderTest &amp;gt; test_with_compact.log 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果比較&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;傳統模式 - 平均每個物件: 78
壓縮模式 - 平均每個物件: 69&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分析&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;傳統模式 (78 bytes):
├─ 物件頭：16 bytes (mark word 8 + klass pointer 8)
├─ int id：4 bytes
├─ String 參考：8 bytes (未壓縮指標)
└─ 對齊填充：~50 bytes (String 物件本身的開銷)

壓縮模式 (69 bytes):
├─ 物件頭：8 bytes (壓縮後)
├─ int id：4 bytes
├─ String 參考：8 bytes
└─ 對齊填充：~49 bytes&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;header 部分減少了 8 bytes，實際上節省 9 bytes&lt;/p&gt;
&lt;p&gt;如果 application 是大量的小物件的集合，例如 cache，就啟用&lt;code&gt;-XX:+UseCompactObjectHeaders&lt;/code&gt;&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/1678501267278289169/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/04/compact-object-header.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/1678501267278289169'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/1678501267278289169'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/04/compact-object-header.html' title='Compact Object Header'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-9070461628228089195</id><published>2026-03-30T08:27:00.002+08:00</published><updated>2026-03-30T08:27:11.037+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>Scoped Values</title><content type='html'>&lt;p&gt;&lt;strong&gt;Scoped Values&lt;/strong&gt; 是一種在特定執行範圍內（scope）安全地共享不可變資料的方法。主要用來取代 ThreadLocal，適合用在 Virtual Thread。&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;threadlocal-vs-scopedvalue&quot;&gt;ThreadLocal vs ScopedValue&lt;/h2&gt;
&lt;p&gt;ThreadLocal&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;為每個 Thread 儲存獨立的可變狀態，每個 &lt;code&gt;Thread&lt;/code&gt; 都有自己的ThreadLocalMap。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ThreadLocal&lt;/code&gt; 狀態同步代價高&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;傳統、可變、不安全於虛擬執行緒&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;需要手動清理 (&lt;code&gt;remove()&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;同一 Thread 被reuse（例如在 Executor 裡）會殘留前一次的資料。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;使用 &lt;strong&gt;Thread Pool&lt;/strong&gt; 或 &lt;strong&gt;Virtual Thread&lt;/strong&gt; 時容易出錯&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;每個 &lt;strong&gt;虛擬執行緒&lt;/strong&gt; 都有自己的 &lt;strong&gt;ThreadLocal Map&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ScopedValue&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;在限定作用範圍中傳遞不可變資料&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;不可變、安全、快速&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;易於清理，離開 scope 即失效。結束作用域後自動清理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;支援虛擬執行緒、巢狀範圍與 Structured Concurrency&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;demo&quot;&gt;Demo&lt;/h2&gt;
&lt;p&gt;如果你希望把 ThreadLocal 值「傳遞」給虛擬執行緒，有兩種方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;手動傳遞&lt;/strong&gt;：在建立虛擬執行緒時，讀取父 ThreadLocal，然後在子執行緒裡設置。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;使用 InheritableThreadLocal&lt;/strong&gt;：Java 提供 &lt;code&gt;InheritableThreadLocal&lt;/code&gt;，可以讓子執行緒自動繼承父執行緒的值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意：對 &lt;strong&gt;虛擬執行緒&lt;/strong&gt;，InheritableThreadLocal 仍然可用，但它只在 &lt;strong&gt;創建時&lt;/strong&gt; 繼承，後續變更不會同步。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import java.util.concurrent.*;
import java.lang.ScopedValue;
import java.time.Duration;

public class ScopedValueVsThreadLocalDemo {

    private static final ThreadLocal&amp;lt;String&amp;gt; TL_USER = new ThreadLocal&amp;lt;&amp;gt;();
    private static final ScopedValue&amp;lt;String&amp;gt; SV_USER = ScopedValue.newInstance();

    private static final InheritableThreadLocal&amp;lt;String&amp;gt; ITL_USER = new InheritableThreadLocal&amp;lt;&amp;gt;();

    public static void main(String[] args) throws Exception {
        System.out.println(&quot;\n=== ThreadLocal 測試 ===&quot;);
        testThreadLocal();

        System.out.println(&quot;\n=== ThreadLocal 測試2 ===&quot;);
        testThreadLocal2();

        System.out.println(&quot;\n=== InheritableThreadLocal 測試3 ===&quot;);
        testThreadLocal3();

        System.out.println(&quot;\n=== ScopedValue 測試 ===&quot;);
        testScopedValue();
    }

    private static void testThreadLocal() throws Exception {
        TL_USER.set(&quot;Alice&quot;);

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            var future = executor.submit(() -&amp;gt; {
                // ThreadLocal 值在 virtual thread 裏面，不會自動繼承
                System.out.println(&quot;[VirtualThread] USER = &quot; + TL_USER.get());
                TL_USER.set(&quot;Cat&quot;);
                TL_USER.remove();
                return null;
            });
            future.get();
        }

        // 離開 VirtualThread 範圍後再取值 -&amp;gt; 還是存在，且已被修改
        System.out.println(&quot;[MainThread] USER = &quot; + TL_USER.get());
        TL_USER.remove();
    }

    private static void testThreadLocal2() throws Exception {
        TL_USER.set(&quot;Alice&quot;);

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            // 讀取父 ThreadLocal
            String parentValue = TL_USER.get();

            // 手動傳遞給虛擬執行緒
            var future = executor.submit(() -&amp;gt; {
                TL_USER.set(parentValue); // 設定子執行緒的值
                System.out.println(&quot;[Child VT] USER = &quot; + TL_USER.get());
                TL_USER.set(&quot;Cat&quot;);
                TL_USER.remove();
                return null;
            });
            future.get();
        }

        System.out.println(&quot;[MainThread] USER = &quot; + TL_USER.get());
        TL_USER.remove();
    }

    private static void testThreadLocal3() throws Exception {
        ITL_USER.set(&quot;Bob&quot;);

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            var future = executor.submit(() -&amp;gt; {
                System.out.println(&quot;[Child VT] ITL_USER = &quot; + ITL_USER.get());
                return null;
            });
            future.get();
        }

        System.out.println(&quot;[MainThread] ITL_USER = &quot; + ITL_USER.get());
        TL_USER.remove();
    }

    private static void testScopedValue() throws Exception {
        ScopedValue.where(SV_USER, &quot;Bob&quot;).run(() -&amp;gt; {
            System.out.println(&quot;[MainScope] USER = &quot; + SV_USER.get());

            try (var scope = StructuredTaskScope.open()) {
                scope.fork(() -&amp;gt; {
                    System.out.println(&quot;[Child VT] USER = &quot; + SV_USER.get());

                    return null;
                });

                scope.join();
            } catch (InterruptedException ie) {
                System.err.println(&quot;StructuredTaskScope: &quot; + ie);
            }
        });

        // 離開 ScopedValue 範圍後再取值 -&amp;gt; 會丟例外
        try {
            System.out.println(&quot;[After Scope] USER = &quot; + SV_USER.get());
        } catch (Exception e) {
            System.out.println(&quot;[After Scope] 無法取得，因為作用域已結束: &quot; + e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;編譯，要加上參數&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;javac --enable-preview --release 25 ScopedValueVsThreadLocalDemo.java&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;java --enable-preview ScopedValueVsThreadLocalDemo&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;=== ThreadLocal 測試 ===
[VirtualThread] USER = null
[MainThread] USER = Alice

=== ThreadLocal 測試2 ===
[Child VT] USER = Alice
[MainThread] USER = Alice

=== InheritableThreadLocal 測試3 ===
[Child VT] ITL_USER = Bob
[MainThread] ITL_USER = Bob

=== ScopedValue 測試 ===
[MainScope] USER = Bob
[Child VT] USER = Bob
[After Scope] 無法取得，因為作用域已結束: java.util.NoSuchElementException: ScopedValue not bound&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/9070461628228089195/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/03/scoped-values.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/9070461628228089195'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/9070461628228089195'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/03/scoped-values.html' title='Scoped Values'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-2702166616981368307</id><published>2026-03-23T08:55:00.001+08:00</published><updated>2026-03-23T08:55:04.044+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>Structure Concurrency</title><content type='html'>&lt;p&gt;是一種讓多執行緒邏輯變得更容易理解與管理的程式設計模型，目標是讓「並行任務的生命週期」像區塊結構（block structure）一樣有明確的範圍。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;子任務（threads, tasks）都&lt;strong&gt;必須在離開某個區塊前結束&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;執行緒之間的層級關係（parent/child）是&lt;strong&gt;語法上明確可見&lt;/strong&gt;的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;不會有「孤兒執行緒」（dangling thread）偷偷跑在背景。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;傳統的-thread&quot;&gt;傳統的 thread&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;任務彼此獨立、缺乏邏輯關聯。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;錯誤傳遞困難（子執行緒例外不會自動向上傳遞）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;難以在結束前確保所有子任務完成。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;void process() {
    Thread t = new Thread(() -&amp;gt; downloadFile());
    t.start();
    // ... do other work ...
    // 忘記 join() 或沒有捕捉例外，就可能造成 thread 泄漏
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;structure-concurrency-jep-453--java-21&quot;&gt;Structure Concurrency (JEP 453 / Java 21)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ShutdownOnFailure&lt;/code&gt;: 若任一子任務失敗，其他會自動中止。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ShutdownOnSuccess&lt;/code&gt;: 若有一個成功，其餘終止（常用於競賽式查詢）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;範圍清晰、錯誤可控、資源可預期。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import java.util.concurrent.*;

void process() throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Future&amp;lt;String&amp;gt; f1 = scope.fork(() -&amp;gt; downloadFile());
        Future&amp;lt;String&amp;gt; f2 = scope.fork(() -&amp;gt; fetchMetadata());
        scope.join();              // 等待所有子任務
        scope.throwIfFailed();     // 傳遞例外
        System.out.println(f1.resultNow() + f2.resultNow());
    } // scope 區塊結束時，自動關閉所有未完成子任務
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Virtual Threads 解決「效能 &amp;amp; 可擴充性」。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Structured Concurrency 解決「邏輯一致性 &amp;amp; 錯誤處理」。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;example&quot;&gt;Example&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;import java.util.concurrent.*;
import java.lang.management.*;

public class StructuredConcurrencyVirtualThreadsExample {

    public static void main(String[] args) throws Exception {
        System.out.println(&quot;Java: &quot; + System.getProperty(&quot;java.version&quot;));
        System.out.println();

        System.out.println(&quot;=== Example 1: Structured Concurrency (ShutdownOnFailure) ===&quot;);
        example();

        System.out.println();
        System.out.println(&quot;=== Example 2: Virtual vs Native Thread Performance Test ===&quot;);
        compareVirtualAndNativeThreads();
    }

    static void example() {
        // 使用 StructuredTaskScope.open() 創建作用域
        try (var scope = StructuredTaskScope.open()) {
            // 創建三個子任務
            var f1 = scope.fork(() -&amp;gt; simulatedIo(&quot;Service-A&quot;, 1200, false));
            var f2 = scope.fork(() -&amp;gt; simulatedIo(&quot;Service-B&quot;, 2500, true));
            var f3 = scope.fork(() -&amp;gt; simulatedIo(&quot;Service-C&quot;, 3000, false));

            // 等待所有子任務完成
            scope.join();

            // 處理結果
            System.out.println(&quot;Results: &quot; +
                (f1.state() == StructuredTaskScope.Subtask.State.SUCCESS ? f1.get() : &quot;Failed&quot;) + &quot;, &quot; +
                (f2.state() == StructuredTaskScope.Subtask.State.SUCCESS ? f2.get() : &quot;Failed&quot;) + &quot;, &quot; +
                (f3.state() == StructuredTaskScope.Subtask.State.SUCCESS ? f3.get() : &quot;Failed&quot;));

        } catch (Exception e) {
            System.out.println(&quot;Scope finished with failure: &quot; + e);
        }
    }

    /**
     * Compare Virtual Threads vs Native Threads with ASCII table output.
     */
    static void compareVirtualAndNativeThreads() throws Exception {
        int taskCount = 10_000;
        System.out.printf(&quot;Launching %,d simulated I/O tasks...\n&quot;, taskCount);

        long nativeTime = measureThreadTypePerformance(taskCount, false);
        long virtualTime = measureThreadTypePerformance(taskCount, true);

        long usedMem = getUsedMemoryMB();

        // ASCII Table output
        System.out.println();
        System.out.println(&quot;+--------------------+----------------+----------------+&quot;);
        System.out.println(&quot;| Thread Type        | Time (ms)      | Observations   |&quot;);
        System.out.println(&quot;+--------------------+----------------+----------------+&quot;);
        System.out.printf (&quot;| %-18s | %-14d | %-14s |%n&quot;, &quot;Native Threads&quot;, nativeTime, &quot;limited pool&quot;);
        System.out.printf (&quot;| %-18s | %-14d | %-14s |%n&quot;, &quot;Virtual Threads&quot;, virtualTime, &quot;scales easily&quot;);
        System.out.println(&quot;+--------------------+----------------+----------------+&quot;);
        System.out.printf (&quot;| %-18s | %-14d | %-14s |%n&quot;, &quot;Heap Used (MB)&quot;, usedMem, &quot;after test&quot;);
        System.out.println(&quot;+--------------------+----------------+----------------+&quot;);
    }

    static long measureThreadTypePerformance(int count, boolean virtual) throws Exception {
        long start = System.currentTimeMillis();

        ExecutorService executor = virtual ? Executors.newVirtualThreadPerTaskExecutor()
                                           : Executors.newFixedThreadPool(200);

        try (executor) {
            for (int i = 0; i &amp;lt; count; i++) {
                int id = i;
                executor.submit(() -&amp;gt; {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException ignored) {}
                    return null;
                });
            }
        }

        long end = System.currentTimeMillis();
        return end - start;
    }

    static String simulatedIo(String name, long millis, boolean fail) throws InterruptedException {
        System.out.printf(&quot;[%s] running on %s, sleep %dms%n&quot;, name, Thread.currentThread(), millis);
        Thread.sleep(millis);
        if (fail) throw new RuntimeException(name + &quot; failed!&quot;);
        return name + &quot;-done&quot;;
    }

    static long getUsedMemoryMB() {
        MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();
        long used = mbean.getHeapMemoryUsage().getUsed();
        return used / (1024 * 1024);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;編譯與執行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;javac --enable-preview --release 25 StructuredConcurrencyVirtualThreadsExample.java
java --enable-preview StructuredConcurrencyVirtualThreadsExample&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;執行結果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;Java: 25

=== Example 1: Structured Concurrency (ShutdownOnFailure) ===
[Service-A] running on VirtualThread[#26]/runnable@ForkJoinPool-1-worker-1, sleep 1200ms
[Service-C] running on VirtualThread[#30]/runnable@ForkJoinPool-1-worker-2, sleep 3000ms
[Service-B] running on VirtualThread[#28]/runnable@ForkJoinPool-1-worker-4, sleep 2500ms
Scope finished with failure: java.util.concurrent.StructuredTaskScope$FailedException: java.lang.RuntimeException: Service-B failed!

=== Example 2: Virtual vs Native Thread Performance Test ===
Launching 10,000 simulated I/O tasks...

+--------------------+----------------+----------------+
| Thread Type        | Time (ms)      | Observations   |
+--------------------+----------------+----------------+
| Native Threads     | 10232          | limited pool   |
| Virtual Threads    | 240            | scales easily  |
+--------------------+----------------+----------------+
| Heap Used (MB)     | 61             | after test     |
+--------------------+----------------+----------------+&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;同時支援舊的-native-thread&quot;&gt;同時支援舊的 Native Thread&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;自動 join 或取消子任務；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;錯誤可統一處理；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;可以使用任意 &lt;code&gt;Executor&lt;/code&gt; 來決定底層執行緒類型。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-java&quot;&gt;    static void example2() {
        try (var scope = StructuredTaskScope.open()) {

            // Virtual Thread Executor
            ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();

            // fork 只能傳 Callable，不傳 Executor
            Callable&amp;lt;String&amp;gt; task1 = () -&amp;gt; simulatedIo(&quot;VT-A&quot;, 500, false);
            Callable&amp;lt;String&amp;gt; task2 = () -&amp;gt; simulatedIo(&quot;VT-B&quot;, 800, false);

            var f1 = scope.fork(task1);
            var f2 = scope.fork(task2);

            // Native Thread Executor
            ExecutorService nativeExecutor = Executors.newFixedThreadPool(2);
            Callable&amp;lt;String&amp;gt; cpuTask = () -&amp;gt; heavyComputation(&quot;NT-A&quot;);
            var f3 = scope.fork(cpuTask);

            scope.join(); // 等待所有子任務完成

            System.out.println(&quot;Results: &quot; + f1.get() + &quot;, &quot; + f2.get() + &quot;, &quot; + f3.get());

            virtualExecutor.shutdown();
        } catch (Exception e) {
            System.out.println(&quot;Scope finished with failure: &quot; + e);
        }

    }

    // 模擬 CPU 任務
    static String heavyComputation(String name) {
        System.out.printf(&quot;[%s] CPU task on %s%n&quot;, name, Thread.currentThread());
        long sum = 0;
        for (long i = 0; i &amp;lt; 10_000_000L; i++) sum += i;
        return name + &quot;-done&quot;;
    }&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/2702166616981368307/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/03/structure-concurrency.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/2702166616981368307'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/2702166616981368307'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/03/structure-concurrency.html' title='Structure Concurrency'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-2640016407818709476</id><published>2026-03-16T08:28:00.002+08:00</published><updated>2026-03-16T08:28:08.960+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="java"/><title type='text'>Java Virtual Thread</title><content type='html'>&lt;p&gt;Java 從 Green Thread 時代開始，演進到使用 OS Thread，然後到了 Java 21 版，正式採用 Virtual Thread，以適應微服務時代伺服器的變革。&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;green-threads&quot;&gt;Green Threads&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;JDK 為了跨平台的特性，要讓程式可以在單核心，沒有多個 thread 的 OS 上運作，所以設計了內建的 thread 跟 scheduler&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;在 JDK 1.1 (1997) 時期，Java 使用 &lt;strong&gt;green threads&lt;/strong&gt;（由 JVM 在使用者空間模擬出來的執行緒，不直接用 OS thread）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;優點：跨平台，不依賴作業系統執行緒，能讓多執行緒程式在沒有多執行緒支援的 OS 上跑。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;缺點：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;JVM 本身要實作 thread scheduler，效能比不上 OS 提供的 thread。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;遇到 &lt;strong&gt;blocking I/O&lt;/strong&gt;（例如 read socket），整個 JVM scheduler 都會被卡住，所有 green threads 都會停住。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;從 JDK 1.3 (2000) 開始，Java 全面切換到 &lt;strong&gt;1:1 OS threads&lt;/strong&gt; 模型&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;native-thread&quot;&gt;Native Thread&lt;/h2&gt;
&lt;p&gt;作業系統核心會用 time-sharing 或 優先權排程 來管理 threads。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;重量級資源單位&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;每個 OS thread 需要 stack (通常 1MB 預設)、TCB (thread control block)、kernel data structures。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;建立/銷毀成本高 (微秒到毫秒級)。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;數量有限&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;即使硬體支援很多核心，實務上 JVM 或應用程式能開的 OS thread 數量大約只有幾千到幾萬。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;blocking I/O 會卡住 thread&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;假設某個 OS thread 正在 &lt;code&gt;read()&lt;/code&gt; 一個 socket，整個 thread 會被 kernel block。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;即使這時 CPU 沒事幹，這個 thread 對 JVM 來說就是「卡住不能用」。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;搶佔式排程 (preemptive)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;OS scheduler 會把 CPU 切給不同 threads。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;context switch 成本高，要切換 CPU 狀態、register、stack。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;共享記憶體&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有 thread 共用同一個 address space，必須透過鎖 (lock, monitor) 控制同步，容易出現 race condition、deadlock。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;OS thread 模型對「大量 I/O 密集型應用」來說效率很差&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;適合少量、CPU 密集的任務&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;不適合製作非常大量的網路連線伺服器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;virtual-thread&quot;&gt;Virtual Thread&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project Loom&lt;/strong&gt; 在 Java 19 引入預覽功能、Java 21 變成正式 GA 的一個重要新功能&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;輕量級&lt;/strong&gt;：建立/銷毀成本非常低，幾 KB stack 就能跑。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;blocking I/O 處理方式不同&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Virtual Thread 呼叫阻塞 API (&lt;code&gt;Socket.read()&lt;/code&gt;)，JVM 會攔截並讓出 OS thread。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;這樣 OS thread 可以拿去執行其他 Virtual Thread，不會浪費資源。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;數量級提升&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;可以開數百萬個 virtual threads，對應數百萬個並發請求。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;讓程式碼還是同步/直觀，但效能接近非同步 I/O 模型。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;簡化程式碼&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不需要複雜的 callback、CompletableFuture、reactive pipeline，直接用同步程式碼就能寫出 scalable 程式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;把「blocking I/O」變得非阻塞化，同時又保留傳統同步 API 的簡單性，讓 Java 更適合現代微服務/高併發應用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;虛擬線程更適合 I/O 型或高併發場景。如果是非常 CPU密集或如果內部有同步鎖 (synchronized) 或重度共享資源競爭，則使用傳統線程。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;erlang-process&quot;&gt;Erlang Process&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;極度輕量&lt;/strong&gt;：一個 process 只佔用幾 KB 記憶體，可以同時開數百萬個。由 BEAM VM 調度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;獨立記憶體空間&lt;/strong&gt;：每個 process 有自己的 heap/stack，不共享狀態。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;排程由 BEAM 虛擬機控制&lt;/strong&gt;：BEAM 用 OS threads（通常一個核心對應一個 scheduler thread）去執行上千萬個 Erlang processes。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;preemptive scheduling&lt;/strong&gt;：Erlang process 執行一定數量的 reductions（指令數）後會自動讓出 CPU。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Erlang process 的定位 比較接近 Java 的 Virtual Thread&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;因為 Erlang 從設計之初就為了 massive concurrency，整個 I/O 模型與錯誤隔離都是以「百萬 process」為目標；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Java Virtual Thread 是在既有 thread-based API 上加的輕量執行緒，強調「低成本封裝既有同步程式碼」。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;erlang-process-vs-java-threads-比較&quot;&gt;Erlang Process vs Java Threads 比較&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Erlang Process&lt;/th&gt;
&lt;th&gt;Java OS Thread&lt;/th&gt;
&lt;th&gt;Java Virtual Thread&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;管理單位&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BEAM VM (使用 scheduler threads)&lt;/td&gt;
&lt;td&gt;OS Kernel&lt;/td&gt;
&lt;td&gt;JVM (基於 OS threads)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;重量級 / 輕量級&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;超輕量 (幾 KB)&lt;/td&gt;
&lt;td&gt;重量級 (MB)&lt;/td&gt;
&lt;td&gt;輕量 (KB 級)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;建立數量&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;百萬級&lt;/td&gt;
&lt;td&gt;幾千～幾萬&lt;/td&gt;
&lt;td&gt;百萬級&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;排程&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BEAM VM preemptive scheduling&lt;/td&gt;
&lt;td&gt;OS scheduler&lt;/td&gt;
&lt;td&gt;JVM scheduler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;blocking I/O&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;不會卡住整個 VM，process 掛起，scheduler 跑其他 process&lt;/td&gt;
&lt;td&gt;卡住 OS thread&lt;/td&gt;
&lt;td&gt;掛起虛擬執行緒，釋放 OS thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;共享記憶體&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;不共享，透過 message passing&lt;/td&gt;
&lt;td&gt;共享，需要鎖&lt;/td&gt;
&lt;td&gt;不共享（但可用同步物件）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;錯誤隔離&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;完整隔離，崩潰不會影響其他 process&lt;/td&gt;
&lt;td&gt;線程崩潰可能拖垮 JVM&lt;/td&gt;
&lt;td&gt;崩潰只影響該 virtual thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;設計哲學&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;為 massive concurrency 與容錯而生&lt;/td&gt;
&lt;td&gt;傳統 multi-threading&lt;/td&gt;
&lt;td&gt;保留同步 API，實現高併發&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;適用場景&lt;/td&gt;
&lt;td&gt;聊天室、遊戲伺服器（每個玩家一個 process）、即時推播（pub/sub 模式）&lt;/td&gt;
&lt;td&gt;I/O 密集、RPC、WebSocket&lt;/td&gt;
&lt;td&gt;CPU 密集、JNI、非阻塞演算法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/2640016407818709476/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/03/java-virtual-thread.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/2640016407818709476'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/2640016407818709476'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/03/java-virtual-thread.html' title='Java Virtual Thread'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-7533852156536248933</id><published>2026-03-09T08:35:00.001+08:00</published><updated>2026-03-09T08:35:07.184+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="linux"/><title type='text'>Shell Script 語法比較表</title><content type='html'>&lt;p&gt;整理常用 Shell (&lt;code&gt;sh&lt;/code&gt;, &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;zsh&lt;/code&gt;) 在語法、功能上的差異與相容性，方便參考。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;類型&lt;/th&gt;
&lt;th&gt;sh (POSIX)&lt;/th&gt;
&lt;th&gt;bash&lt;/th&gt;
&lt;th&gt;zsh&lt;/th&gt;
&lt;th&gt;備註&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shebang&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#!/bin/sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#!/bin/bash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#!/bin/zsh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;建議腳本跨平台用 &lt;code&gt;#!/bin/sh&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;變數宣告&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name=value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;不能有空格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;字串插值&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&quot;Hello $name&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;都支援&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;命令替換&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;`date`&lt;/code&gt; 或 &lt;code&gt;$(date)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;建議用 &lt;code&gt;$( )&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;條件判斷&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ &quot;$a&quot; = &quot;$b&quot; ]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ &quot;$a&quot; = &quot;$b&quot; ]&lt;/code&gt; 或 &lt;code&gt;[[ $a == $b ]]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ &quot;$a&quot; = &quot;$b&quot; ]&lt;/code&gt; 或 &lt;code&gt;[[ $a = $b ]]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[[ ... ]]&lt;/code&gt; 不是 POSIX 的寫法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;邏輯運算&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ &quot;$a&quot; = 1 ] &amp;amp;&amp;amp; [ &quot;$b&quot; = 2 ]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[[ $a = 1 &amp;amp;&amp;amp; $b = 2 ]]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[[ ... &amp;amp;&amp;amp; ... ]]&lt;/code&gt; 非 POSIX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;數學運算&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$((1+2))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;((i++))&lt;/code&gt; / &lt;code&gt;$((1+2))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;((i++))&lt;/code&gt; / &lt;code&gt;$((1+2))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POSIX sh 只能 &lt;code&gt;$(( ))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;陣列&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 不支援&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arr=(a b c)&lt;/code&gt; → &lt;code&gt;${arr[0]}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arr=(a b c)&lt;/code&gt; → &lt;code&gt;${arr[1]}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bash 陣列從 0 起算，Zsh 從 1 起算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;關聯陣列&lt;/strong&gt;&amp;nbsp;key=value&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;code&gt;declare -A map; map[key]=val&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;typeset -A map; map[key]=val&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POSIX sh 沒有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;brace expansion&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1..5}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1..5}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POSIX sh 不支援&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;迴圈 for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;for i in 1 2 3; do ...; done&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;for i in {1..3}; do ...; done&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;brace expansion 會展開整個序列&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1..3}&lt;/code&gt; 不是 POSIX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;函數定義&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;foo() { ... }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;foo() { ... }&lt;/code&gt; 或 &lt;code&gt;function foo { ... }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;&lt;code&gt;function foo {}&lt;/code&gt; 不是 POSIX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;字串長度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;${#var}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;字串比較&lt;/td&gt;
&lt;td&gt;&lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;==&lt;/code&gt; / &lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;==&lt;/code&gt; / &lt;code&gt;=&lt;/code&gt; (glob)&lt;/td&gt;
&lt;td&gt;POSIX sh 只能用 &lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大小寫轉換&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;code&gt;${var^^}&lt;/code&gt; / &lt;code&gt;${var,,}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;${(U)var}&lt;/code&gt; / &lt;code&gt;${(L)var}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bash/Zsh 特殊功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;字串切割&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;${var%pattern}&lt;/code&gt; / &lt;code&gt;${var#pattern}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;支援更多：&lt;code&gt;${var^^}&lt;/code&gt; (大寫)&lt;/td&gt;
&lt;td&gt;支援更多：&lt;code&gt;:${(U)var}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POSIX 只有 &lt;code&gt;%&lt;/code&gt; &lt;code&gt;#&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;printf / echo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;printf&lt;/code&gt; 標準，&lt;code&gt;echo&lt;/code&gt; 不一定支援 &lt;code&gt;-e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;echo -e&lt;/code&gt; 可用&lt;/td&gt;
&lt;td&gt;&lt;code&gt;echo -e&lt;/code&gt; 可能無效，用 &lt;code&gt;print&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;建議用 &lt;code&gt;printf&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;測試檔案&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ -f file ]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;正則比對&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[[ string =~ regex ]]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[[ string =~ regex ]]&lt;/code&gt; 但 regex 語法不同&lt;/td&gt;
&lt;td&gt;POSIX sh 無 regex 功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;展開 (globbing)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基本 &lt;code&gt;* ? [ ]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;需 &lt;code&gt;shopt -s globstar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zsh 預設更強大 (e.g. &lt;code&gt;**/*.txt&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Zsh glob 功能最強&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;補全 (tab)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;需 &lt;code&gt;bash-completion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;內建強大補全&lt;/td&gt;
&lt;td&gt;互動環境差異，不影響 script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;錯誤處理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;set -e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相同，加強版 &lt;code&gt;set -o pipefail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pipefail&lt;/code&gt; 不是 POSIX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;信號處理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;trap &#39;cmd&#39; INT TERM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;相同&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;source 檔案&lt;/td&gt;
&lt;td&gt;&lt;code&gt;. file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source file&lt;/code&gt; / &lt;code&gt;. file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source file&lt;/code&gt; / &lt;code&gt;. file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POSIX sh 用 &lt;code&gt;.&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;目錄堆疊&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pushd&lt;/code&gt; / &lt;code&gt;popd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pushd&lt;/code&gt; / &lt;code&gt;popd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POSIX sh 無目錄堆疊功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;互動功能&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;readline、history、completion&lt;/td&gt;
&lt;td&gt;history、completion、prompt customization&lt;/td&gt;
&lt;td&gt;Zsh 提供最強互動功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local / typeset&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt; / &lt;code&gt;typeset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt; / &lt;code&gt;typeset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POSIX sh 不支援函數內局部變數&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;sh&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;POSIX，相容性最好，但功能有限。&lt;/li&gt;
&lt;li&gt;如果要製作跨平台的 script，就使用標準的 POSIX 語法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;bash&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;增強 POSIX，支援陣列、關聯陣列、&lt;code&gt;[[ ]]&lt;/code&gt;、brace expansion、Bashisms。&lt;/li&gt;
&lt;li&gt;一般在 mac/linux，可使用 bash script&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;zsh&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;幾乎包含 Bash 功能，互動功能更強（prompt、補全、glob、history）&lt;/li&gt;
&lt;li&gt;陣列索引從 1 開始，部分語法行為不同。&lt;/li&gt;
&lt;li&gt;使用者互動操作時，可使用 zsh&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/7533852156536248933/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/03/shell-script.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/7533852156536248933'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/7533852156536248933'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/03/shell-script.html' title='Shell Script 語法比較表'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-5707739768812000720</id><published>2026-03-02T08:31:00.002+08:00</published><updated>2026-03-02T08:31:23.877+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="linux"/><title type='text'>zsh</title><content type='html'>&lt;p&gt;在 RockyLinux 測試 zsh。目前比較常見的，還是使用 bash，如果要測試 zsh，需要另外安裝。&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;安裝&quot;&gt;安裝&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;dnf -y install zsh&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改預設的 shell&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;chsh -s /bin/zsh&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;基本的設定檔&quot;&gt;基本的設定檔&lt;/h2&gt;
&lt;p&gt;可直接跳到下面的 Oh My Zsh&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# 1. 基本環境
export LANG=en_US.UTF-8
export EDITOR=vim
export PATH=&quot;/usr/local/bin:$PATH&quot;

# 2. Prompt 設定
PROMPT=&#39;[%*]%n@%m %~$ &#39;

# 3. Alias 常用指令
alias ll=&#39;ls -lh&#39;
alias la=&#39;ls -la&#39;
alias ..=&#39;cd ..&#39;
alias gs=&#39;git status&#39;

# 4. 歷史設定
HISTSIZE=5000
SAVEHIST=5000
HISTFILE=~/.zsh_history
setopt share_history      # 多個 zsh session 共享歷史

# 5. 自動補全 &amp;amp; 修正
autoload -Uz compinit &amp;amp;&amp;amp; compinit
setopt correct            # 拼字錯誤自動建議
setopt autocd             # 輸入資料夾名稱自動 cd
setopt nocaseglob         # 補全時忽略大小寫

# 6. 補全行為微調
zstyle &#39;:completion:*&#39; matcher-list &#39;m:{a-z}={A-Z}&#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;功能有&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;乾淨的 prompt&lt;/strong&gt; → &lt;code&gt;[時間]使用者@主機 當前路徑$&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;常用 alias&lt;/strong&gt; → &lt;code&gt;ll&lt;/code&gt;, &lt;code&gt;la&lt;/code&gt;, &lt;code&gt;gs&lt;/code&gt;, &lt;code&gt;..&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;歷史&lt;/strong&gt; → 儲存 5000 筆並共享不同視窗&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;自動補全&lt;/strong&gt; → tab 補全，支援大小寫不敏感&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;錯字修正 &amp;amp; auto cd&lt;/strong&gt; → 輸入資料夾名稱會自動進去&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;oh-my-zsh&quot;&gt;Oh My Zsh&lt;/h2&gt;
&lt;p&gt;Oh My Zsh 是 zsh 常見的 plugin 管理工具，可安裝多個 plugin 擴充 zsh 的功能，但也要注意，載入越多 plugin 會讓 zsh 啟動變慢。&lt;/p&gt;
&lt;p&gt;安裝&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;sh -c &quot;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常見的 theme 工具是 Powerlevel10k，但這邊不使用，直接用基本的 Prompt&lt;/p&gt;
&lt;p&gt;修改 &lt;code&gt;~/.zshrc&lt;/code&gt; 增加以下這個部分，然後就能將一些不同用途的設定檔分開&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;for file in .zshrc_*;
do source $file;
done&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;增加常用-plugins&quot;&gt;增加常用 plugins&lt;/h2&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;安裝-1&quot;&gt;安裝&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;cd $ZSH/custom/plugins
# 安裝 zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-autosuggestions.git

# 安裝 zsh-syntax-highlighting
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git

# zsh-completions
git clone https://github.com/zsh-users/zsh-completions&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;atx&quot; id=&quot;設定檔&quot;&gt;設定檔&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;~/.zshrc_basic&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# 1. 指定 Oh My Zsh 安裝路徑
export ZSH=&quot;$HOME/.oh-my-zsh&quot;

# 3. Plugins
# git           → git alias &amp;amp; 補全
# zsh-autosuggestions → 歷史自動建議
# zsh-syntax-highlighting → 命令語法高亮
# history-substring-search → 部分字串歷史搜索
# extract       → 快速解壓縮
plugins=(
  git
  zsh-autosuggestions
  zsh-syntax-highlighting
  zsh-completions
  history-substring-search
  extract
  colored-man-pages
  z
)

# 4. 啟動 Oh My Zsh
source $ZSH/oh-my-zsh.sh

# 5. PATH &amp;amp; 編輯器
export PATH=&quot;/usr/local/bin:$PATH&quot;
export EDITOR=vim
export LANG=en_US.UTF-8

# 6. Alias 常用指令
alias ll=&#39;ls -lh&#39;
#alias la=&#39;ls -A&#39;
alias la=&#39;ls -la&#39;
alias ..=&#39;cd ..&#39;
alias gs=&#39;git status&#39;
alias gp=&#39;git push&#39;
alias gd=&#39;git diff&#39;

# 7. 歷史設定
HISTSIZE=5000
SAVEHIST=5000
HISTFILE=~/.zsh_history
setopt share_history      # 多個 zsh session 共享歷史

# 8. 自動補全 &amp;amp; 行為優化
autoload -Uz compinit &amp;amp;&amp;amp; compinit
setopt correct            # 拼字錯誤自動建議
setopt autocd             # 輸入資料夾名稱自動 cd
setopt nocaseglob         # 補全大小寫不敏感
zstyle &#39;:completion:*&#39; matcher-list &#39;m:{a-z}={A-Z}&#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外做一個 &lt;code&gt;~/.zshrc_prompt&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;#PROMPT=&#39;%F{green}[%D{%H:%M}]%f%F{yellow}%n@%m%f %F{blue}%~%f$ &#39;
#PROMPT=&#39;%F{green}[%D{%H:%M}]%f%F{yellow}%n@%m%f %F{blue}%~%f${git_prompt_info} $ &#39;
autoload -Uz vcs_info
precmd() { vcs_info }
zstyle &#39;:vcs_info:git:*&#39; formats &#39;(%b)&#39;

PROMPT=&#39;%F{blue}[%D{%H:%M}]%f%F{green}%n@%m%f %F{magenta}%~%f${vcs_info_msg_0_} $ &#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;增加功能，區分本地跟遠端的 ssh&lt;/p&gt;
&lt;p&gt;Zsh / Bash 都可以靠環境變數來判斷：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;本地登入&lt;/strong&gt; -&amp;gt; &lt;code&gt;SSH_CONNECTION&lt;/code&gt; / &lt;code&gt;SSH_TTY&lt;/code&gt; &lt;strong&gt;不存在&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;遠端登入 (SSH)&lt;/strong&gt; -&amp;gt; &lt;code&gt;SSH_CONNECTION&lt;/code&gt; 或 &lt;code&gt;SSH_TTY&lt;/code&gt; &lt;strong&gt;存在&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;if [[ -n &quot;$SSH_CONNECTION&quot; ]]; then
  HOST_STYLE=&quot;%F{red}@%m%f(ssh)&quot;
else
  HOST_STYLE=&quot;%F{yellow}@%m%f&quot;
fi

PROMPT=&#39;%F{blue}[%D{%H:%M}]%f%F{green}%n%f&#39;&quot;$HOST_STYLE&quot;&#39; %F{magenta}%~%f${vcs_info_msg_0_} $ &#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新登入後，就可以使用 zsh&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;extract-plugin&quot;&gt;extract plugin&lt;/h2&gt;
&lt;p&gt;extract 定義了一個名為 extract 的函數，用於解壓縮你傳遞給它的檔案，並支援多種檔案類型。&lt;/p&gt;
&lt;p&gt;使用方式為 &lt;code&gt;extract 檔案名稱&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;不需要知道具體的解壓縮命令，只需執行 extract  就可以解壓大部分常見檔案，直接輸入 &lt;code&gt;x 檔案名稱&lt;/code&gt; 也可以&lt;/p&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;z-plugin&quot;&gt;z plugin&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;Usage: z [OPTION]... [ARGUMENT]
Jump to a directory that you have visited frequently or recently, or a bit of both, based on the partial
string ARGUMENT.

With no ARGUMENT, list the directory history in ascending rank.

  --add Add a directory to the database
  -c    Only match subdirectories of the current directory
  -e    Echo the best match without going to it
  -h    Display this help and exit
  -l    List all matches without going to them
  -r    Match by rank
  -t    Match by recent access
  -x    Remove a directory from the database (by default, the current directory)
  -xR   Remove a directory and its subdirectories from the database (by default, the current directory)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;autojump&quot;&gt;autojump&lt;/h2&gt;
&lt;p&gt;z 是用 shell script 實作，autojump 是 python，z 的速度比較快，且內建於 zsh。一般建議就直接使用 z，如果要使用 autojump，需要做另外的設定。&lt;/p&gt;
&lt;p&gt;安裝&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;cd $ZSH/custom/plugins

# 安裝 autojump
git clone https://github.com/wting/autojump.git&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 autojump 需要另外安裝套件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;dnf -y install autojump&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因為 autojump 只有提供 bash 版本的初始化 script&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block&quot;&gt;/usr/share/autojump/autojump.bash&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;現在要使用 zsh，故要另外產生一個 zsh 版本的初始化&lt;/p&gt;
&lt;p&gt;到 autojump 原始的 github 網頁取得 &lt;a href=&quot;https://raw.githubusercontent.com/wting/autojump/refs/heads/master/bin/autojump.zsh&quot;&gt;autojump.zsh&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;把 autojump.zsh 放到 /usr/share/autojump/autojump.zsh&lt;/p&gt;
&lt;p&gt;修改設定檔&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;plugins=(
  autojump
)

# Autojump 初始化
# 確認 autojump 安裝後再 source
if [ -f /usr/share/autojump/autojump.zsh ]; then
  source /usr/share/autojump/autojump.zsh
fi&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-bash&quot;&gt;# 任意切換目錄
cd ~/download/dir1
cd ~/download/dir2

# 檢查 autojump
j -s

# autojump
j dir1&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/5707739768812000720/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/03/zsh.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/5707739768812000720'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/5707739768812000720'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/03/zsh.html' title='zsh'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9289096.post-4843018328610984212</id><published>2026-02-09T08:25:00.004+08:00</published><updated>2026-02-09T08:25:38.769+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="javascript"/><title type='text'>Cytoscape.js</title><content type='html'>&lt;p&gt;&lt;a href=&quot;https://js.cytoscape.org/&quot;&gt;Cytoscape.js&lt;/a&gt; 是一個處理資料視覺化的 javascript library，當我們要對資料關係進行可視化顯示時，例如社交網路關係或網路拓樸圖時，Cytoscape.js 是個不錯的選擇。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cytoscape.org/&quot;&gt;Cytoscape&lt;/a&gt; 和 Cytoscape.js 是兩個完全獨立不同的軟體&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Cytoscape&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;使用 Java 語言編寫的用於網絡可視化的桌面應用程序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;需要安裝 Java SDK 才能使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;用於大型網絡分析和可視化的高性能應用程序&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cytoscape.js&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;用於網絡可視化的 javascript library，本身不是一個完整的 Web Application&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;可以在大多數瀏覽器上使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;不需要 plugin 即可運行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;需要編寫程式來建構 Web Application&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;支援 Extensions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;基於 CSS 將資料映射到元件屬性&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;sample1&quot;&gt;sample1&lt;/h2&gt;
&lt;p&gt;建立一個 &lt;strong&gt;圓形排列&lt;/strong&gt; 的 4 個節點 (A, B, C, D)，節點之間有箭頭連線，點擊節點會有事件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;zh-Hant&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;test1&amp;lt;/title&amp;gt;
    &amp;lt;script src=&quot;https://unpkg.com/cytoscape/dist/cytoscape.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
            display: block;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h2&amp;gt;test1&amp;lt;/h2&amp;gt;
    &amp;lt;div id=&quot;cy&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
        const cy = cytoscape({
            container: document.getElementById(&#39;cy&#39;),

            elements: [
                { data: { id: &#39;a&#39;, label: &#39;節點 A&#39; } },
                { data: { id: &#39;b&#39;, label: &#39;節點 B&#39; } },
                { data: { id: &#39;c&#39;, label: &#39;節點 C&#39; } },
                { data: { id: &#39;d&#39;, label: &#39;節點 D&#39; } },
                { data: { id: &#39;ab&#39;, source: &#39;a&#39;, target: &#39;b&#39; } },
                { data: { id: &#39;bc&#39;, source: &#39;b&#39;, target: &#39;c&#39; } },
                { data: { id: &#39;cd&#39;, source: &#39;c&#39;, target: &#39;d&#39; } },
                { data: { id: &#39;da&#39;, source: &#39;d&#39;, target: &#39;a&#39; } }
            ],

            style: [
                {
                    selector: &#39;node&#39;,
                    style: {
                        &#39;background-color&#39;: &#39;#0074D9&#39;,
                        &#39;label&#39;: &#39;data(label)&#39;,
                        &#39;color&#39;: &#39;#fff&#39;,
                        &#39;text-valign&#39;: &#39;center&#39;,
                        &#39;text-outline-width&#39;: 2,
                        &#39;text-outline-color&#39;: &#39;#0074D9&#39;
                    }
                },
                {
                    selector: &#39;edge&#39;,
                    style: {
                        &#39;width&#39;: 3,
                        &#39;line-color&#39;: &#39;#AAAAAA&#39;,
                        &#39;target-arrow-color&#39;: &#39;#AAAAAA&#39;,
                        &#39;target-arrow-shape&#39;: &#39;triangle&#39;,
                        &#39;curve-style&#39;: &#39;bezier&#39;,
                    }
                }
            ],

            layout: {
                name: &#39;circle&#39;
            }
        });

        // 點擊事件
        cy.on(&#39;tap&#39;, &#39;node&#39;, function(evt) {
            let node = evt.target;
            console.log(&#39;你點了節點: &#39; + node.id());
        });
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;sample2&quot;&gt;sample2&lt;/h2&gt;
&lt;p&gt;流程圖，layout 調整為 dagre extension。&lt;/p&gt;
&lt;p&gt;使用時要引用 dagre library 及 Cytoscape.js 的 extension&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;dagre&lt;/strong&gt; 正是 Cytoscape.js 常用來畫flowchart或 directed graph 的 layout。適合做 flowchart, network topology, workflow&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;zh-Hant&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Cytoscape.js Flowchart&amp;lt;/title&amp;gt;
    &amp;lt;script src=&quot;https://unpkg.com/cytoscape/dist/cytoscape.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;https://unpkg.com/dagre/dist/dagre.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;https://unpkg.com/cytoscape-dagre/cytoscape-dagre.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h2&amp;gt;flowchart&amp;lt;/h2&amp;gt;
    &amp;lt;div id=&quot;cy&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
        cytoscape.use(cytoscapeDagre);

        const cy = cytoscape({
            container: document.getElementById(&#39;cy&#39;),

            elements: [
                { data: { id: &#39;start&#39;, label: &#39;開始&#39; } },
                { data: { id: &#39;step1&#39;, label: &#39;步驟 1&#39; } },
                { data: { id: &#39;step2&#39;, label: &#39;步驟 2&#39; } },
                { data: { id: &#39;decision&#39;, label: &#39;判斷 ?&#39; } },
                { data: { id: &#39;end&#39;, label: &#39;結束&#39; } },
                { data: { id: &#39;s1&#39;, source: &#39;start&#39;, target: &#39;step1&#39; } },
                { data: { id: &#39;s2&#39;, source: &#39;step1&#39;, target: &#39;step2&#39; } },
                { data: { id: &#39;s3&#39;, source: &#39;step2&#39;, target: &#39;decision&#39; } },
                { data: { id: &#39;s4&#39;, source: &#39;decision&#39;, target: &#39;end&#39; } },
                { data: { id: &#39;s5&#39;, source: &#39;decision&#39;, target: &#39;step1&#39; } }
            ],

            style: [
                {
                    selector: &#39;node&#39;,
                    style: {
                        &#39;shape&#39;: &#39;round-rectangle&#39;,
                        &#39;background-color&#39;: &#39;#28a745&#39;,
                        &#39;label&#39;: &#39;data(label)&#39;,
                        &#39;color&#39;: &#39;#fff&#39;,
                        &#39;text-valign&#39;: &#39;center&#39;,
                        &#39;text-outline-width&#39;: 2,
                        &#39;text-outline-color&#39;: &#39;#28a745&#39;
                    }
                },
                {
                    selector: &#39;node[id=&quot;decision&quot;]&#39;,
                    style: {
                        &#39;shape&#39;: &#39;diamond&#39;,
                        &#39;background-color&#39;: &#39;#ffc107&#39;,
                        &#39;text-outline-color&#39;: &#39;#ffc107&#39;
                    }
                },
                {
                    selector: &#39;edge&#39;,
                    style: {
                        &#39;width&#39;: 2,
                        &#39;line-color&#39;: &#39;#555&#39;,
                        &#39;target-arrow-color&#39;: &#39;#555&#39;,
                        &#39;target-arrow-shape&#39;: &#39;triangle&#39;,
                        &#39;curve-style&#39;: &#39;bezier&#39;,
                    }
                }
            ],

            layout: {
                name: &#39;dagre&#39;,
                // rankDir: &#39;TB&#39;  // top-to-bottom
                rankDir: &#39;LR&#39; // 由左到右 排列
            }
        });
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class=&quot;atx&quot; id=&quot;sample3&quot;&gt;sample3&lt;/h2&gt;
&lt;p&gt;鐵路模擬，增加火車在鐵軌上移動的動畫&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;fenced-code-block language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;zh-Hant&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Cytoscape.js Railway&amp;lt;/title&amp;gt;
    &amp;lt;script src=&quot;https://unpkg.com/cytoscape/dist/cytoscape.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h2&amp;gt;Railway&amp;lt;/h2&amp;gt;
    &amp;lt;div id=&quot;cy&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
        const cy = cytoscape({
            container: document.getElementById(&#39;cy&#39;),

            elements: [
                { data: { id: &#39;station1&#39;, label: &#39;車站 1&#39; } },
                { data: { id: &#39;station2&#39;, label: &#39;車站 2&#39; } },
                { data: { id: &#39;station3&#39;, label: &#39;車站 3&#39; } },
                { data: { id: &#39;checkpoint4&#39;, label: &#39;檢查點 4&#39; } },
                { data: { id: &#39;checkpoint5&#39;, label: &#39;檢查點 5&#39; } },
                { data: { id: &#39;checkpoint6&#39;, label: &#39;檢查點 6&#39; } },
                { data: { id: &#39;s1&#39;, source: &#39;station1&#39;, target: &#39;station2&#39; } },
                { data: { id: &#39;s2&#39;, source: &#39;station2&#39;, target: &#39;station3&#39; } },
                { data: { id: &#39;s3&#39;, source: &#39;station2&#39;, target: &#39;checkpoint4&#39; } },
                { data: { id: &#39;s4&#39;, source: &#39;checkpoint4&#39;, target: &#39;checkpoint5&#39; } },
                { data: { id: &#39;s5&#39;, source: &#39;checkpoint5&#39;, target: &#39;station3&#39; } },
                { data: { id: &#39;s6&#39;, source: &#39;checkpoint5&#39;, target: &#39;checkpoint6&#39; } },

                // 列車節點
                { data: { id: &#39;train1&#39;, label: &#39;🚆&#39; }, classes: &#39;train&#39; }
            ],

            style: [
                {
                    selector: &#39;node&#39;,
                    style: {
                        &#39;shape&#39;: &#39;ellipse&#39;,
                        &#39;background-color&#39;: &#39;#0074D9&#39;,
                        &#39;label&#39;: &#39;data(label)&#39;,
                        &#39;color&#39;: &#39;#fff&#39;,
                        &#39;text-valign&#39;: &#39;center&#39;,
                        &#39;text-outline-width&#39;: 2,
                        &#39;text-outline-color&#39;: &#39;#0074D9&#39;
                    }
                },
                {
                    selector: &#39;node[id^=&quot;station&quot;]&#39;,
                    style: {
                        &#39;shape&#39;: &#39;round-rectangle&#39;,
                        &#39;background-color&#39;: &#39;#17a2b8&#39;,
                        &#39;text-outline-color&#39;: &#39;#17a2b8&#39;
                    }
                },
                {
                    selector: &#39;node.train&#39;,
                    style: {
                        &#39;background-color&#39;: &#39;red&#39;,
                        &#39;shape&#39;: &#39;ellipse&#39;,
                        &#39;label&#39;: &#39;data(label)&#39;,
                        &#39;font-size&#39;: 24,
                        &#39;width&#39;: 30,
                        &#39;height&#39;: 30
                    }
                },
                {
                    selector: &#39;edge&#39;,
                    style: {
                        &#39;width&#39;: 2,
                        &#39;line-color&#39;: &#39;#555&#39;,
                        &#39;target-arrow-color&#39;: &#39;#555&#39;,
                        &#39;target-arrow-shape&#39;: &#39;triangle&#39;
                    }
                }
            ],

            layout: {
                name: &#39;breadthfirst&#39;,
                directed: true,
                padding: 20
            }
        });

        function moveAlongEdge(train, fromNode, toNode, duration, callback) {
            const start = fromNode.position();
            const end = toNode.position();
            const startTime = performance.now();

            function animate(now) {
                const elapsed = now - startTime;
                const t = Math.min(elapsed / duration, 1); // 0~1
                const x = start.x + (end.x - start.x) * t;
                const y = start.y + (end.y - start.y) * t;
                train.position({ x, y });

                if (t &amp;lt; 1) {
                    requestAnimationFrame(animate);
                } else if (callback) {
                    callback();
                }
            }

            requestAnimationFrame(animate);
        }

        function moveTrain(path) {
            let i = 0;
            const train = cy.getElementById(&#39;train1&#39;);

            function step() {
                if (i &amp;gt;= path.length - 1) return;
                const fromNode = cy.getElementById(path[i]);
                const toNode = cy.getElementById(path[i + 1]);

                moveAlongEdge(train, fromNode, toNode, 2000, () =&amp;gt; {
                    i++;
                    step();
                });
            }

            step();
        }

        // 定義路徑
        const route = [&#39;station1&#39;, &#39;station2&#39;, &#39;checkpoint4&#39;, &#39;checkpoint5&#39;, &#39;station3&#39;];

        // 初始化列車位置
        cy.getElementById(&#39;train1&#39;).position(cy.getElementById(route[0]).position());

        // 2 秒後啟動列車
        setTimeout(() =&amp;gt; moveTrain(route), 2000);
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://cctg.blogspot.com/feeds/4843018328610984212/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://cctg.blogspot.com/2026/02/cytoscapejs.html#comment-form' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/4843018328610984212'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9289096/posts/default/4843018328610984212'/><link rel='alternate' type='text/html' href='http://cctg.blogspot.com/2026/02/cytoscapejs.html' title='Cytoscape.js'/><author><name>cctg</name><uri>http://www.blogger.com/profile/05516073474557428585</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry></feed>