繼 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 等諸多特性。本文将介紹此次更新最重要的特性——子產品系統。
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技術棧在背景回複Java擷取一系列Java新特性教程。
這個看似微小的改變,到底可以帶來哪些好處?在我看來,至少帶來四方面的好處。
第一,原生的依賴管理。
有了子產品系統,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 關鍵詞提供的服務實作類。
public class EventCenter {
public static void main(String[] args) throws ReflectiveOperationException {
System.out.println("Demo: Direct Mode");
var listeners = new ArrayList<IEventListener>();
listeners.add(new EchoListener());
listeners.add((IEventListener<String>) Class.forName("mod2a.opens.ReflectEchoListener").getDeclaredConstructor().newInstance());
var event = Events.newEvent();
listeners.forEach(l -> l.onEvent(event));
System.out.println();
System.out.println("Demo: SPI Mode");
var listeners2 = ServiceLoader.load(IEventListener.class).stream().map(ServiceLoader.Provider::get).collect(Collectors.toList());
var event2 = Events.newEvent();
listeners2.forEach(l -> l.onEvent(event2));
}
}
代碼-1: mod1.EventCenter.java
指令行下執行
./build_mods.sh
,得到輸出如下,結果和預期一緻。
Demo: Direct Mode
[echo] Event received: 68eb4671-c057-4bc2-9653-c31f5e3f72d2
[reflect echo] Event received: 68eb4671-c057-4bc2-9653-c31f5e3f72d2
Demo: SPI Mode
[spi echo] Event received: 678d239a-77ef-4b7f-b7aa-e76041fcdf47
[echo] Event received: 678d239a-77ef-4b7f-b7aa-e76041fcdf47
[reflect echo] Event received: 678d239a-77ef-4b7f-b7aa-e76041fcdf47
[internal echo] Event received: 678d239a-77ef-4b7f-b7aa-e76041fcdf47
代碼-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子產品化政策
假設初始時,所有 jar 包都是非子產品化的,此時應用運作指令為:
java -cp mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar mod1.EventCenter
對 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 參數的作用就是手動指定額外的根子產品,這樣應用就可以正常運作了。
接着完成 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 參數裡了。
最後完成 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 子產品系統的介紹就到這裡了,歡迎你到我的留言闆分享,和大家一起過過招。更多請關注公衆号Java技術棧在背景回複Java擷取一系列Java新特性教程,下期再見。
版權聲明:本文為CSDN部落客「eMac」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。原文連結:
https://blog.csdn.net/eMac/article/details/1071314446 參考
https://www.ibm.com/developerworks/cn/java/the-new-features-of-Java-9/index.html) https://openjdk.java.net/jeps/261 http://tutorials.jenkov.com/java/modules.html https://www.oracle.com/corporate/features/understanding-java-9-modules.html https://www.oracle.com/java/java9-screencasts.html https://www.cnblogs.com/IcanFixIt/p/6947763.html