繼 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 升級系列的第一篇,本文將介紹這次升級最重要的特性——模塊系統。
若是把 Java 8 比做單體應用,那麼引入模塊系統以後,從 Java 9 開始,Java 就華麗的轉身爲微服務。模塊系統,項目代號 Jigsaw,最先於 2008 年 8 月提出(比 Martin Fowler 提出微服務還早 6 年),2014 年跟隨 Java 9 正式進入開發階段,最終跟隨 Java 9 發佈於 2017 年 9 月。html
那麼什麼是模塊系統?官方的定義是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
包含如下信息:java
import
使用)圖-1: Java 9 Modulegit
也就是說,任意一個 jar 文件,只要加上一個合法的 module descriptor
,就能夠升級爲一個模塊。這個看似微小的改變,到底能夠帶來哪些好處?在我看來,至少帶來四方面的好處。github
第一,原生的依賴管理。有了模塊系統,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
關鍵詞,模塊維護者就精準控制哪些類能夠對外開放使用,哪些類只能內部使用,換句話說就是再也不依賴文檔,而是由編譯器來保證。類可見性的細化,除了帶來更好的兼容性,也帶來了更好的安全性。oracle
圖-3: Java Accessibility框架
第四,提高 Java 語言開發效率。Java 9 以後,Java 像開掛了通常,一改原先一延再延的風格,嚴格遵循每半年一個大版本的發佈策略,從 2017 年 9 月到 2020 年 3 月,從 Java 9 到 Java 14,三年時間相繼發佈了 6 個版本,無一延期,參見圖-4。這無疑跟模塊系統的引入有莫大關係。前文提到,Java 9 以後,JDK 被拆分爲 94 個模塊,每一個模塊有清晰的邊界(module descriptor
)和獨立的單元測試,對於每一個 Java 語言的開發者而言,每一個人只須要關注其所負責的模塊,開發效率所以大幅提高。這其中的差異,就比如單體應用架構升級到微服務架構通常,版本迭代速度不快也難。ide
圖-4: Java SE Lifecycle
上面提到,模塊的核心在於 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 服務的全部實現類。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>
爲了幫助你理解 module descriptor
語法和新的 Java 參數,我專門設計了一個示例工程,其內包含了 5 個模塊:
IEventListener
),並聲明瞭一個未公開的服務實現類。圖-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 { // 方式1:經過exports和opens System.out.println("Demo: Direct Mode"); var listeners = new ArrayList<IEventListener>(); // 使用導出類 listeners.add(new EchoListener()); // 使用開放類 // compile error: listeners.add(new ReflectEchoListener()); listeners.add((IEventListener<String>) Class.forName("mod2a.opens.ReflectEchoListener").getDeclaredConstructor().newInstance()); var event = Events.newEvent(); listeners.forEach(l -> l.onEvent(event)); System.out.println(); // 方式2:經過SPI System.out.println("Demo: SPI Mode"); // 加載全部的IEventListener實現類,無視其導出/開放與否 var listeners2 = ServiceLoader.load(IEventListener.class).stream().map(ServiceLoader.Provider::get).collect(Collectors.toList()); // compile error: listeners.add(new InternalEchoListener()); // compile error: listeners.add(new SpiEchoListener()); 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 結果輸出
看到這裏,相信建立和運行一個新的模塊應用對你而言已經不是問題了,可問題是老的 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 文件都出如今類路徑下,也就是轉爲未命名模塊,對於未命名模塊而言,默認導出全部包而且依賴全部模塊,所以應用能夠正常運行。進一步的解讀能夠參閱官方白皮書的相關章節。
基於未命名模塊和自動模塊,相應的就產生了兩種老版本應用的遷移策略,或者說模塊化策略。
第一種策略,叫作自底向上(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,沒有 mod三、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 爲主模塊(也是根模塊),所以全部其餘模塊根據依賴關係都會被識別爲可觀察到的模塊並加入到運行時環境,應用能夠正常運行。
自底向上策略很容易理解,實施路徑也很清晰,但它有一個隱含的假設,即全部 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
,但其中只有 mod一、mod2a、mod2b、mod3 是真正的模塊,mod4 未作任何改造,藉由模塊系統轉爲自動模塊。
圖-9: Top-down模塊化策略
看上去很完美,不過等一下,若是有多個自動模塊,而且它們之間存在分裂包呢?前面提到,自動模塊和其它命名模塊同樣,須要遵循分裂包規則。對於這種狀況,若是模塊化改造勢在必行,要麼忍痛割愛精簡依賴只保留其中的一個自動模塊,要麼本身動手豐衣足食 Hack 一個版本。固然,你也能夠試試找到這些自動模塊的維護者們,讓他們 PK 一下決定誰纔是這個分裂包的主人。
有關模塊系統的介紹到這就基本結束了,簡單回顧一下,首先我介紹了什麼是模塊、模塊化的好處,接着給出了定義模塊的語法,和編譯、運行模塊的命令,並輔以一個示例工程進行說明,最後詳細闡述了老版本應用模塊化改造的思路。如今咱們再來看一些跟模塊系統比較類似的框架和工具,以進一步加深你對模塊系統的理解。
提及模塊化,尤爲在 Java 界,那麼確定繞不過 OSGi 這個模塊系統的鼻祖。OSGi 裏的 bundle 跟模塊系統裏的模塊很是類似,都是以 jar 文件的形式存在,每一個 bundle 有本身的名稱,也會定義依賴的 bundle、導出的包、發佈的服務等。所不一樣的是,OSGi bundle 能夠定義版本,還有生命週期的概念,包括 installed、resolved、uninstalled、starting、active、stopping 6 種狀態,全部 bundle 都由 OSGi 容器進行管理,而且在同一個 OSGi 容器裏面容許同時運行同一個 bundle 的多個版本,甚至每一個 bundle 有各自獨立的 classloader。以上種種特性使得 OSGi 框架變得很是重,在微服務盛行的當下,愈來愈被邊緣化。
Maven 的依賴管理和模塊系統存在一些類似之處,Maven 裏的 artifact 對應模塊 ,都是以 jar 文件的形式存在,有名稱,能夠聲明傳遞依賴。不一樣之處在於,Maven artifact 支持版本,但缺乏包一級的信息,也沒有服務的概念。若是 Java 一出生就帶有模塊系統,那麼 Maven 的依賴管理大機率就會直接基於模塊系統來設計了。
ArchUnit 在包可見性方面的控制能力和模塊系統相比,有過之而無不及,而且能夠細化到類、方法、屬性這一級。但 ArchUnit 缺乏模塊一級的控制,模塊系統的出現正好補齊了 ArchUnit 這一方面的短板,二者相輔相成、相得益彰,之後落地架構規範也省了不少口水。
若是你能看到這裏,恭喜你已經贏了 90% 的讀者。爲了表揚你的耐心,免費贈送一個小彩蛋,給你一個 jar 文件,如何用最快的速度判別它是否是一個模塊?它又是如何定義的?試試看 jar -d -f <jar_file>
。
有關 Java 模塊系統的介紹就到這裏了,歡迎你到個人留言板分享,和你們一塊兒過過招。下期再見。