繼 2014 年 3 月 Java 8 發布之后,時隔 4 年,2018 年 9 月,Java 11 如期發布,其間間隔了 Java 9 和 Java 10 兩個非LTS(Long Term Support)版本。作為最新的LTS版本,相比 Java 8,Java 11 包含了模塊系統、改用 G1 作為默認 GC 算法、反應式流 Flow、新版 HttpClient 等諸多特性。作為 JDK 11 升級系列的第一篇,本文將介紹此次升級最重要的特性——模塊系統。
1 模塊系統簡介
如果把 Java 8 比作單體應用,那么引入模塊系統之后,從 Java 9 開始,Java 就華麗的轉身為微服務。模塊系統,項目代號 Jigsaw,最早於 2008 年 8 月提出(比 Martin Fowler 提出微服務還早 6 年),2014 年跟隨 Java 9 正式進入開發階段,最終跟隨 Java 9 發布於 2017 年 9 月。
那么什么是模塊系統?官方的定義是A uniquely named, reusable group of related packages, as well as resources (such as images and XML files) and a module descriptor.
如圖-1所示,模塊的載體是 jar 文件,一個模塊就是一個 jar 文件,但相比於傳統的 jar 文件,模塊的根目錄下多了一個 module-info.class
文件,也即 module descriptor
。 module descriptor
包含以下信息:
- 模塊名稱
- 依賴哪些模塊
- 導出模塊內的哪些包(允許直接
import
使用) - 開放模塊內的哪些包(允許通過 Java 反射訪問)
- 提供哪些服務
- 依賴哪些服務
圖-1: Java 9 Module
也就是說,任意一個 jar 文件,只要加上一個合法的 module descriptor
,就可以升級為一個模塊。這個看似微小的改變,到底可以帶來哪些好處?在我看來,至少帶來四方面的好處。
第一,原生的依賴管理。有了模塊系統,Java 可以根據 module descriptor
計算出各個模塊間的依賴關系,一旦發現循環依賴,啟動就會終止。同時,由於模塊系統不允許不同模塊導出相同的包(即 split package
,分裂包),所以在查找包時,Java 可以精准的定位到一個模塊,從而獲得更好的性能。
第二,精簡 JRE。引入模塊系統之后,JDK 自身被划分為 94 個模塊(參見圖-2)。通過 Java 9 新增的 jlink
工具,開發者可以根據實際應用場景隨意組合這些模塊,去除不需要的模塊,生成自定義 JRE,從而有效縮小 JRE 大小。得益於此,JRE 11 的大小僅為 JRE 8 的 53%,從 218.4 MB縮減為 116.3 MB,JRE 中廣為詬病的巨型 jar 文件 rt.jar
也被移除。更小的 JRE 意味着更少的內存占用,這讓 Java 對嵌入式應用開發變得更友好。
圖-2: The Modular JDK
第三,更好的兼容性。自打 Java 出生以來,就只有 4 種包可見性,這讓 Java 對面向對象的三大特征之一封裝的支持大打折扣,類庫維護者對此叫苦不迭,只能一遍又一遍的通過各種文檔或者奇怪的命名來強調這些或者那些類僅供內部使用,擅自使用后果自負雲雲。Java 9 之后,利用 module descriptor
中的 exports
關鍵詞,模塊維護者就精准控制哪些類可以對外開放使用,哪些類只能內部使用,換句話說就是不再依賴文檔,而是由編譯器來保證。類可見性的細化,除了帶來更好的兼容性,也帶來了更好的安全性。
圖-3: Java Accessibility
第四,提升 Java 語言開發效率。Java 9 之后,Java 像開掛了一般,一改原先一延再延的風格,嚴格遵循每半年一個大版本的發布策略,從 2017 年 9 月到 2020 年 3 月,從 Java 9 到 Java 14,三年時間相繼發布了 6 個版本,無一延期,參見圖-4。這無疑跟模塊系統的引入有莫大關系。前文提到,Java 9 之后,JDK 被拆分為 94 個模塊,每個模塊有清晰的邊界(module descriptor
)和獨立的單元測試,對於每個 Java 語言的開發者而言,每個人只需要關注其所負責的模塊,開發效率因此大幅提升。這其中的差別,就好比單體應用架構升級到微服務架構一般,版本迭代速度不快也難。
圖-4: Java SE Lifecycle
2 基礎篇
2.1 module descriptor
上面提到,模塊的核心在於 module descriptor
,對應根目錄下的 module-info.class
文件,而這個 class 文件是由源代碼根目錄下的 module-info.java
編譯生成。Java 為 module-info.java
設計了專用的語法,包含 module
、 requires
、exports
等多個關鍵詞(參見圖-5)。
圖-5: module-info.java 語法
語法解讀:
[open] module <module>
: 聲明一個模塊,模塊名稱應全局唯一,不可重復。加上open
關鍵詞表示模塊內的所有包都允許通過 Java 反射訪問,模塊聲明體內不再允許使用opens
語句。requires [transitive] <module>
: 聲明模塊依賴,一次只能聲明一個依賴,如果依賴多個模塊,需要多次聲明。加上transitive
關鍵詞表示傳遞依賴,比如模塊 A 依賴模塊 B,模塊 B 傳遞依賴模塊 C,那么模塊 A 就會自動依賴模塊 C,類似於 Maven。exports <package> [to <module1>[, <module2>...]]
: 導出模塊內的包(允許直接import
使用),一次導出一個包,如果需要導出多個包,需要多次聲明。如果需要定向導出,可以使用to
關鍵詞,后面加上模塊列表(逗號分隔)。opens <package> [to <module>[, <module2>...]]
: 開放模塊內的包(允許通過 Java 反射訪問),一次開放一個包,如果需要開放多個包,需要多次聲明。如果需要定向開放,可以使用to
關鍵詞,后面加上模塊列表(逗號分隔)。provides <interface | abstract class> with <class1>[, <class2> ...]
: 聲明模塊提供的 Java SPI 服務,一次可以聲明多個服務實現類(逗號分隔)。uses <interface | abstract class>
: 聲明模塊依賴的 Java SPI 服務,加上之后模塊內的代碼就可以通過ServiceLoader.load(Class)
一次性加載所聲明的 SPI 服務的所有實現類。
2.2 -p & -m 參數
Java 9 引入了一系列新的參數用於編譯和運行模塊,其中最重要的兩個參數是 -p
和 -m
。-p
參數指定模塊路徑,多個模塊之間用 “:”(Mac, Linux)或者 “;”(Windows)分隔,同時適用於 javac
命令和 java
命令,用法和Java 8 中的 -cp
非常類似。-m
參數指定待運行的模塊主函數,輸入格式為模塊名/主函數所在的類名
,僅適用於 java
命令。兩個參數的基本用法如下:
-
javac -p <module_path> <source>
-
java -p <module_path> -m <module>/<main_class>
2.3 Demo 示例
為了幫助你理解 module descriptor
語法和新的 Java 參數,我專門設計了一個示例工程,其內包含了 5 個模塊:
- mod1 模塊: 主模塊,展示了使用服務實現類的兩種方式。
- mod2a 模塊: 分別導出和開放了一個包,並聲明了兩個服務實現類。
- mod2b 模塊: 聲明了一個未公開的服務實現類。
- mod3 模塊: 定義 SPI 服務(
IEventListener
),並聲明了一個未公開的服務實現類。 - mod4 模塊: 導出公共模型類。
圖-6: 包含 5 個模塊的示例工程
先來看一下主函數,方式 1 展示了直接使用 mod2 導出和開放的兩個 IEventListener
實現類,方式 2 展示了通過 Java SPI 機制使用所有的 IEventListener
實現類,無視其導出/開放與否。方式 2 相比 方式 1,多了兩行輸出,分別來自於 mod2b 和 mod3 通過 provides
關鍵詞提供的服務實現類。
1 |
public class EventCenter { |
代碼-1: mod1.EventCenter.java
命令行下執行./build_mods.sh
,得到輸出如下,結果和預期一致。
1 |
Demo: Direct Mode |
代碼-2: EventCenter 結果輸出
3 進階篇
看到這里,相信創建和運行一個新的模塊應用對你而言已經不是問題了,可問題是老的 Java 8 應用怎么辦?別着急,我們先來了解兩個高級概念,未命名模塊(unnamed module)和自動模塊(automatic module)。
圖-7: 未命名模塊 vs 自動模塊
一個未經模塊化改造的 jar 文件是轉為未命名模塊還是自動模塊,取決於這個 jar 文件出現的路徑,如果是類路徑,那么就會轉為未命名模塊,如果是模塊路徑,那么就會轉為自動模塊。注意,自動模塊也屬於命名模塊的范疇,其名稱是模塊系統基於 jar 文件名自動推導得出的,比如 com.foo.bar-1.0.0.jar 文件推導得出的自動模塊名是 com.foo.bar。圖-7列舉了未命名模塊和自動模塊行為上的區別,除此之外,兩者還有一個關鍵區別,分裂包規則適用於自動模塊,但對未命名模塊無效,也即多個未命名模塊可以導出同一個包,但自動模塊不允許。
未命名模塊和自動模塊存在的意義在於,無論傳入的 jar 文件是否一個合法的模塊(包含 module descriptor
),Java 內部都可以統一的以模塊的方式進行處理,這也是 Java 9 兼容老版本應用的架構原理。運行老版本應用時,所有 jar 文件都出現在類路徑下,也就是轉為未命名模塊,對於未命名模塊而言,默認導出所有包並且依賴所有模塊,因此應用可以正常運行。進一步的解讀可以參閱官方白皮書的相關章節。
基於未命名模塊和自動模塊,相應的就產生了兩種老版本應用的遷移策略,或者說模塊化策略。
3.1 Bottom-up 自底向上策略
第一種策略,叫做自底向上(bottom-up)策略,即根據 jar 包依賴關系(如果依賴關系比較復雜,可以使用 jdeps
工具進行分析),沿着依賴樹自底向上對 jar 包進行模塊化改造(在 jar 包的源代碼根目錄下添加合法的模塊描述文件 module-info.java
)。初始時,所有 jar 包都是非模塊化的,全部置於類路徑下(轉為未命名模塊),應用以傳統方式啟動。然后,開始自底向上對 jar 包進行模塊化改造,改造完的 jar 包就移到模塊路徑下,這期間應用仍以傳統方式啟動。最后,等所有 jar 包都完成模塊化改造,應用改為 -m
方式啟動,這也標志着應用已經遷移為真正的 Java 9 應用。以上面的示例工程為例,
圖-8: Bottom-up模塊化策略
1) 假設初始時,所有 jar 包都是非模塊化的,此時應用運行命令為:
java -cp mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar mod1.EventCenter
2) 對 mod3 和 mod4 進行模塊化改造。完成之后,此時 mod1, mod2a, mod2b 還是普通的 jar 文件,新的運行命令為:
java -cp mod1.jar:mod2a.jar:mod2b.jar -p mod3.jar:mod4.jar --add-modules mod3,mod4 mod1.EventCenter
對比上一步的命令,首先 mod3.jar 和 mod4.jar 從類路徑移到了模塊路徑,這個很好理解,因為這兩個 jar 包已經改造成了真正的模塊。其次,多了一個額外的參數 --add-modules mod3,mod4
,這是為什么呢?這就要談到模塊系統的模塊發現機制了。
不管是編譯時,還是運行時,模塊系統首先都要確定一個或者多個根模塊(root module),然后從這些根模塊開始根據模塊依賴關系在模塊路徑中循環找出所有可觀察到的模塊(observable module),這些可觀察到的模塊加上類路徑下的 jar 文件最終構成了編譯時環境和運行時環境。那么根模塊是如何確定的呢?對於運行時而言,如果應用是通過 -m
方式啟動的,那么根模塊就是 -m
指定的主模塊;如果應用是通過傳統方式啟動的,那么根模塊就是所有的 java.*
模塊即 JRE(參見圖-2)。回到前面的例子,如果不加 --add-modules
參數,那么運行時環境中除了 JRE 就只有 mod1.jar、mod2a.jar、mod2b.jar,沒有 mod3、mod4 模塊,就會報 java.lang.ClassNotFoundException
異常。如你所想,--add-modules
參數的作用就是手動指定額外的根模塊,這樣應用就可以正常運行了。
3) 接着完成 mod2a、mod2b 的模塊化改造,此時運行命令為:
java -cp mod1.jar -p mod2a.jar:mod2b.jar:mod3.jar:mod4.jar --add-modules mod2a,mod2b,mod4 mod1.EventCenter
由於 mod2a、mod2b 都依賴 mod3,所以 mod3 就不用加到 --add-modules
參數里了。
4) 最后完成 mod1 的模塊化改造,最終運行命令就簡化為:
java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter
注意此時應用是以 -m
方式啟動,並且指定了 mod1 為主模塊(也是根模塊),因此所有其他模塊根據依賴關系都會被識別為可觀察到的模塊並加入到運行時環境,應用可以正常運行。
3.2 Top-down 自上而下策略
自底向上策略很容易理解,實施路徑也很清晰,但它有一個隱含的假設,即所有 jar 包都是可以模塊化的,那如果其中有 jar 包無法進行模塊化改造(比如 jar 包是一個第三方類庫),怎么辦?別慌,我們再來看第二種策略,叫做自上而下(top-down)策略。
它的基本思路是,根據 jar 包依賴關系,從主應用開始,沿着依賴樹自上而下分析各個 jar 包模塊化改造的可能性,將 jar 包分為兩類,一類是可以改造的,一類是無法改造的。對於第一類,我們仍然采用自底向上策略進行改造,直至主應用完成改造,對於第二類,需要從一開始就放入模塊路徑,即轉為自動模塊。這里就要談一下自動模塊設計的精妙之處,首先,自動模塊會導出所有包,這樣就保證第一類 jar 包可以照常訪問自動模塊,其次,自動模塊依賴所有命名模塊,並且允許訪問所有未命名模塊的類(這一點很重要,因為除自動模塊之外,其它命名模塊是不允許訪問未命名模塊的類),這樣就保證自動模塊自身可以照常訪問其他類。等到主應用完成模塊化改造,應用的啟動方式就可以改為 -m
方式。
還是以示例工程為例,假設 mod4 是一個第三方 jar 包,無法進行模塊化改造,那么最終改造完之后,雖然應用運行命令和之前一樣還是java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter
,但其中只有 mod1、mod2a、mod2b、mod3 是真正的模塊,mod4 未做任何改造,借由模塊系統轉為自動模塊。
圖-9: Top-down模塊化策略
看上去很完美,不過等一下,如果有多個自動模塊,並且它們之間存在分裂包呢?前面提到,自動模塊和其它命名模塊一樣,需要遵循分裂包規則。對於這種情況,如果模塊化改造勢在必行,要么忍痛割愛精簡依賴只保留其中的一個自動模塊,要么自己動手豐衣足食 Hack 一個版本。當然,你也可以試試找到這些自動模塊的維護者們,讓他們 PK 一下決定誰才是這個分裂包的主人。
4 番外篇
有關模塊系統的介紹到這就基本結束了,簡單回顧一下,首先我介紹了什么是模塊、模塊化的好處,接着給出了定義模塊的語法,和編譯、運行模塊的命令,並輔以一個示例工程進行說明,最后詳細闡述了老版本應用模塊化改造的思路。現在我們再來看一些跟模塊系統比較相似的框架和工具,以進一步加深你對模塊系統的理解。
4.1 vs OSGi
說起模塊化,尤其在 Java 界,那么肯定繞不過 OSGi 這個模塊系統的鼻祖。OSGi 里的 bundle 跟模塊系統里的模塊非常相似,都是以 jar 文件的形式存在,每個 bundle 有自己的名稱,也會定義依賴的 bundle、導出的包、發布的服務等。所不同的是,OSGi bundle 可以定義版本,還有生命周期的概念,包括 installed、resolved、uninstalled、starting、active、stopping 6 種狀態,所有 bundle 都由 OSGi 容器進行管理,並且在同一個 OSGi 容器里面允許同時運行同一個 bundle 的多個版本,甚至每個 bundle 有各自獨立的 classloader。以上種種特性使得 OSGi 框架變得非常重,在微服務盛行的當下,越來越被邊緣化。
4.2 vs Maven
Maven 的依賴管理和模塊系統存在一些相似之處,Maven 里的 artifact 對應模塊 ,都是以 jar 文件的形式存在,有名稱,可以聲明傳遞依賴。不同之處在於,Maven artifact 支持版本,但缺少包一級的信息,也沒有服務的概念。如果 Java 一出生就帶有模塊系統,那么 Maven 的依賴管理大概率就會直接基於模塊系統來設計了。
4.3 vs ArchUnit
ArchUnit 在包可見性方面的控制能力和模塊系統相比,有過之而無不及,並且可以細化到類、方法、屬性這一級。但 ArchUnit 缺少模塊一級的控制,模塊系統的出現正好補齊了 ArchUnit 這一方面的短板,兩者相輔相成、相得益彰,以后落地架構規范也省了很多口水。
5 彩蛋
如果你能看到這里,恭喜你已經贏了 90% 的讀者。為了表揚你的耐心,免費贈送一個小彩蛋,給你一個 jar 文件,如何用最快的速度判別它是不是一個模塊?它又是如何定義的?試試看 jar -d -f <jar_file>
。
有關 Java 模塊系統的介紹就到這里了,歡迎你到我的留言板分享,和大家一起過過招。下期再見。
6 參考
- Java 9 新特性概述
- JEP 261: Module System
- Java Modules
- Understanding Java 9 Modules
- Java 9 Expert Insights
- Java 9 揭秘(2. 模塊化系統)
轉載:
http://emacoo.cn/coding/java-module-system/