![]() |
VOOZH | about |
軟體可以看成是一個由相互交互的部件組成的系統。在Java中,人們普遍將那些組件中的每一個都打包成自己的JAR文件。理論上講,一個部件包含三個屬性:名稱、供外界使用的公共API和對其他部件的依賴。這種跟圖類似的模型有助於開發人員和工具剖視、分析和使用軟體系統。
但是,那些屬性在Java運行時內部並不存在,它是使用類路徑來訪問一大堆JAR文件,然後簡單地將它們統統揉成一個巨大的泥團。JAR文件之間的所有差別都完全丟失了,只剩下一個程序包的扁平集合,從而失去了進一步驗證的能力。因此,有一些重要的問題是運行中的JVM無法回答的,例如,「這些都是必須的JAR文件嗎?」,「恰恰就是這些JAR文件嗎?」,「存在衝突嗎?」,或者「是只使用了公共API嗎?」
因此,一方面,關於如何將系統模塊化以及系統各部件之間的依賴關係,已經存在一個結構良好的模型。另一方面,實際的運行時環境是一個單一的、幾乎完全沒有結構的命名空間。這種不匹配導致了大家甚為熟悉的JAR地獄及對內部API的依賴,還導致了啟動性能低下及脆弱的安全性。
Jigsaw項目將對編譯器和運行時進行增強,以便向結構化模型靠攏。它的主要目標是可靠配置(通過聲明依賴)和強封裝(通過隱藏內部構件),兩者的代理即是模塊的概念。
模塊簡介下面這段話援引自我重點推薦的、Oracle首席架構師Mark Reinhold編寫的設計概覽「模塊系統的狀態」:
模塊是一個命名的、自描述的代碼和數據集合。其代碼組織成一個包含類型(即Java類和接口)的程序包;其數據包含資源和其他類型的靜態信息。
為了控制代碼如何引用其他模塊的類型,模塊會聲明它編譯和運行時所需要的其他模塊。為了控制其他模塊的代碼引用其程序包里的類型的方式,模塊會聲明哪些程序包可以輸出。
相關贊助商
ArchSummit深圳2016將於7月15-16在華僑城洲際大酒店舉行,3月20日前立減2040元!
因此,與JAR文件相比,模塊有一個可被JVM識別的名稱,聲明了它依賴的其他模塊,定義了哪些程序包是其公共API的組成部分。
名稱模塊可以隨意命名,但必須不能衝突。為此,建議使用標準的程序包反向域名模式。雖然這不是強制的,但這通常意味著模塊名是它所包含的程序包的前綴。
依賴&可讀性模塊列出了它編譯和運行時依賴的其他模塊。下面這段話也是援引自「模塊系統的狀態」:
當一個模塊直接依賴於另一個模塊[……]那麼第一個模塊中的代碼將能夠引用第二個模塊中的類型。我們因此可以說,第一個模塊讀取第二個模塊,或者,等效的說法,第二個模塊對第一個模塊而言是可讀的。
[…]
模塊系統確保每個依賴都恰好有另一個模塊滿足這種依賴,任何兩個模塊都不能互相讀,每個模塊至多只能讀一個定義了特定程序包的模塊,定義了同名程序包的模塊互不妨礙。
可讀性概念是可靠配置的基礎:違反任何條件,模塊系統都會拒絕編譯或運行代碼;這是對脆弱的類路徑模型的一個重大改進。
輸出&可訪問性模塊列出了它輸出的程序包。一個模塊中的類型只能被另一個模塊中滿足如下條件的代碼訪問:
這就是說,公開的不一定是真公開的。一個非輸出程序包中的公開類型同一個輸出程序包中的非公開類型一樣,對外界而言都是隱藏的。因此,「public」現在甚至比程序包私有類型隱藏得都深,因為模塊系統甚至不允許通過反射訪問。就Jigsaw目前的實現而言,關於這一點,命令行標識是唯一的方式。
因此,可訪問性建立在可讀性和輸出語句的基礎上,是強封裝的基礎。所謂強封裝,是指模塊作者可以明確表達公開和支持模塊API的哪些部分。
示例:創建我們的第一個模塊比如說,在我們的網絡中,有一個監控微服務的應用程式。它周期性地同微服務通信,並使用它們的應答更新一個資料庫表及一個簡潔的JavaFX UI。眼下,我們假設應用程式是作為一個獨立的項目開發的,沒有任何依賴。
現在,讓我們切換到帶來Jigsaw的Java 9!(早期訪問版本可以從java.net上下載——本文介紹的示例代碼和命令都是針對2015年12月22日的build 96創建的。)雖然,Java依賴分析器jdeps在JDK 8中就已經存在,但我們需要使用JDK 9版本,因為它知道模塊。
首先需要注意的是,我們可以簡單地忽略模塊。除非代碼依賴於內部API或者其他一些JDK實現細節(在這種情況下,代碼可能遭到破壞),應用程式完全可以同使用Java 8一樣編譯和運行。只需要將工件(如果有的話還有它的依賴)添加到類路徑並調用main為了將代碼移入模塊,我們必須為它創建一個模塊描述符。這是一個名為module-info.java的原始碼文件,位於原始碼目錄的根目錄下:module com.infoq.monitor { // 添加應用程式需要的模塊 // 添加應用程式輸出的程序包
}
現在,我們的應用程式是一個名為com.infoq.monitor的模塊了。我們可以使用jdeps確定它依賴哪些模塊:
jdeps -module
ServiceMonitor.jar這個命令會列出應用程式用到的所有程序包,更重要的是,這些程序包的來源模塊:java.base、java.logging、java.sql、javafx.base、javafx.controls、javafx.graphics。
模塊中已經包含了應用程式所依賴的程序包,我們現在可以考慮可能輸出哪些程序包了。由於我們正在談論的是一個獨立應用程式,我們不會輸出任何東西。
module com.infoq.monitor { requires java.base; // 後文會有進一步的介紹 requires java.logging; requires java.sql; requires javafx.base; requires javafx.controls; requires javafx.graphics; // 不輸出任何包
}編譯同不用Jigsaw一樣,只是需要在源文件列表中包含文件:由於所有的類都已經編譯到jar -c \ --file=mods/com.infoq.monitor.jar \ --main-class=com.infoq.monitor.Monitor \ ${compiled class
files}新的--main-class標識用於指定包含應用程式入口的類。編譯結果就是一個所謂的模塊化JAR文件,下面我們會進一步討論。與舊模型形成鮮明對比的是,應用程式啟動採用一個全新的指令序列。我們使用新的-mp開關指定查找模塊的位置,使用-m指定我們想要啟動的模塊:java -mp
mods -m com.infoq.monitor
為了更好地理解這一點,我們將探討下編譯器和虛擬機如何處理模塊。
Java和模塊模塊種類JDK本身就是模塊化的,包含大約80個平台模塊(可以通過java -listmods查看)。那些模塊將會標準化,Java 9 SE專屬的模塊將以「java.」為前綴,JDK專屬的模塊將以「jdk.」為前綴。
不是所有的Java環境都必須包含所有的平台模塊。相反,Jigsaw的其中一個目標就是可擴展平台,就是說可以輕鬆創建一個只包含所需模塊的運行時。
所有的Java代碼均依賴於Object,而且幾乎所有的代碼都使用像線程和集合這樣的基本特性。這些類型在java.base中提供,因此後者扮演了一個特殊的角色;它是唯一一個模塊系統本來就知道的模塊,由於所有的代碼都依賴它,所以所有模塊都會自動讀它。
因此,在上面的例子中,我們不需要聲明對java.base的依賴。
非平台模塊稱為應用程式模塊,用於啟動應用程式的模塊(包含main方法的模塊)稱為初始化模塊。
模塊化JAR文件前面我們已經看到,Jigsaw還是創建JAR文件,儘管有新的語義。如果它們包含一個module-info.class文件,那麼它們就被稱為模塊化JAR文件,關於這一點,「模塊系統的狀態」一文是這樣描述的:
除了它在根目錄下包含一個module-info.class文件之外,模塊化JAR文件在所有可能的方面都同普通的JAR文件類似。
模塊化JAR文件既可以置於類路徑中,也可以置於模塊路徑中(見下文)。這使得項目可以發布成一個單獨的工件,讓用戶決定是採用舊有的應用程式構建方法,還是模塊化方法。
模塊路徑平台模塊是當前使用的環境的組成部分,因此很容易獲取。為了讓編譯器或虛擬機知道應用程式模塊,我們必須使用-mp指定模塊路徑(就像上文所做的那樣)。
當前環境中的平台模塊同模塊路徑中的應用程式模塊一起構成了可見模塊空間。有了這個模塊集合和一個包含其中的初始化模塊,虛擬機就可以創建一個模塊圖。
模塊圖從初始化應用程式模塊開始,模塊系統解析所有的傳遞依賴。解析結果是一副模塊圖,其中模塊是節點,一個模塊對另一個模塊的依賴是一條有向邊。
(點擊放大圖像)
其中,藍色為平台模塊,亮一些的為直接依賴模塊,暗一些的為傳遞依賴模塊。無所不在的java.base沒有顯示;記住,所有的模塊都隱式依賴這個模塊。
示例:劃分模塊基於對編譯器和虛擬機如何處理模塊的理解,我們開始考慮如何將應用程式劃分成模塊。
我們的應用程式架構包含以下幾個部分:
我們接下來為每個部分創建一個模塊:
Jigsaw快速入門指南和JDK本身都建議為每個模塊在項目原始碼文件夾根目錄下創建一個文件夾。在我們的示例中,目錄結構如下:
Service Monitor └─ src ├─ com.infoq.monitor │ ├─ com ... │ └module-info.java ├─ com.infoq.monitor.db │ ├─ com ... │ └module-info.java ├─ com.infoq.monitor.stats │ ├─ com ... │ └module-info.java
└─ com.infoq.monitor.ui ├─ com ... └module-info.java
這裡的目錄樹經過了裁剪,但每個「com」目錄代表同名的程序包,並且會包含更多的子目錄以及最終的模塊代碼。
統計模塊依賴java.base(但正如我們所已經了解的那樣,我們不必列出它),並使用Java內建的日誌工具。它是一個公開的API,包含在一個單獨的程序包里,允許請求聚合和詳細的統計信息。
module com.infoq.monitor.stats { requires java.logging; exports com.infoq.monitor.stats.get; }
再重申下可訪問性原則:com.infoq.monitor.stats.get中非公開的類型以及其他程序包中的所有類型對其他模塊而言都是隱藏的。即使是輸出的程序包,也只對讀這個模塊的模塊可見。
我們的資料庫模塊也記錄日誌,而且明顯需要Java的SQL特性。它的API包含一個簡單的寫入器:
module com.infoq.monitor.db { requires java.logging; requires java.sql; exports com.infoq.monitor.db.write; }
很明顯,用戶界面需要包含用到的JavaFX特性的平台模塊。它的API包含啟動JavaFX UI的方法。這會返回一個使用了JavaFX特性的模型。客戶端可以更新這個模型,而UI會顯示新的狀態:
module com.infoq.monitor.ui { requires javafx.base; requires javafx.controls; requires javafx.graphics; exports com.infoq.monitor.ui.launch; exports com.infoq.monitor.ui.show; }
現在,我們已經討論完了實際的功能,可以將注意力轉移到將所有這些部件連接在一起的主模塊上了。該模塊需要前面介紹過的三個模塊以及Java的日誌工具。由於UI的依賴模塊需要使用JavaFX特性,所以它還依賴於javafx.base。由於主模塊不會被其他模塊使用,所以沒有輸出API。
module com.infoq.monitor { requires com.infoq.monitor.stats; requires com.infoq.monitor.db; requires com.infoq.monitor.ui; requires javafx.base; //為了更新UI模型 requires java.logging; // 沒有輸出程序包
}
我們不得不顯式聲明需要javafx.base程序包,這有點笨拙,但是如果我們不這樣做,那麼我們就無法從javafx.beans.property調用任何代碼。為此,Jigsaw原型提供了隱式可讀性的概念,我們將稍後介紹。
(點擊放大圖像)
現在,讓我們看下部分用於編譯、打包和啟動剛剛模塊化了的應用程式的命令。對於那些除了JDK本身之外沒有依賴的模塊,下面就是我們編譯和打包模塊的方式:
javac -d classes/com.infoq.monitor.stats ${source files} jar -c \ --file=mods/com.infoq.monitor.stats.jar \ ${compiled class files}同以前一樣,跟Java
8完全相同,我們編譯模塊的原始碼,將結果文件寫進classes文件夾下以模塊名命名的子目錄,並在mods目錄創建一個JAR文件。這是一個模塊化JAR文件,因為類文件中包含編譯好的模塊描述符module-info.class。javac \ -mp mods \ -d classes/com.infoq.monitor \ ${list of source
files} jar -c \ --file=mods/com.infoq.monitor.jar \ --main-class=com.infoq.monitor.Monitor \ ${compiled class files} java -mp mods -m com.infoq.monitor編譯器需要我們前面創建的應用程式模塊,我們通過-mp
modes指定模塊路徑以將其指向那裡。打包和啟動同以前一樣,但是現在,目錄mods包含了不只一個模塊,而是四個。
JVM將通過查找模塊com.infoq.monitor啟動,因為我們將其指定為初始化模塊。當JVM找到這個模塊,它會嘗試解析可見模塊(在這個例子中,包含四個應用程式模塊和所有的平台模塊)空間中的所有依賴,包括直接依賴和傳遞依賴。
如果JVM能夠構建出一個有效的模塊圖,那麼它最後會查找使用--main-class=com.infoq.monitor.Monitor指定的main方法,並啟動應用程式。否則,它會失敗並拋出異常,通知我們違反的條件,例如,缺少一個模塊或者一個循環依賴。隱式可讀性
一個模塊依賴於另一個模塊有兩種形式。
一種是內部消費的依賴,外界不知道它們的存在。以Guava為例,依賴於該項目某個模塊的代碼根本就不關心它內部是否使用了不可變列表。
這是最常見的情況,上文介紹的可讀性已經涵蓋這種情況,一個模塊只能在聲明了對另一個模塊的依賴後才能訪問該模塊的API。因此,如果一個模塊依賴Guava,那麼其他模塊對這個事實一無所知,如果它們自己不顯式聲明對Guava的依賴就無法訪問它。
但是,還有一種情況,依賴沒有完全封裝,而是存在於模塊之間的邊界上。在那種情況下,一個模塊依賴於另一個,並在自己公開的API中暴露了被依賴模塊的類型。在Guava的例子中,一個模塊暴露的方法可能需要或者返回一個不可變列表。
因此,想要調用依賴模塊的代碼也許只能使用被調用模塊的類型。但是如果它沒有同時讀第二個模塊,那麼它就不能那樣做。因此,為了能夠使用依賴模塊,客戶模塊全都不得不同時顯式聲明對第二個模塊的依賴。確定並手工解決這樣的隱藏依賴是一項乏味而容易出錯的工作。
這就是隱式可讀性的用途所在了:
[我們]擴展了模塊聲明,以便模塊可以將可讀性授予另外的模塊,將它所依賴的模塊的可讀性授予任何依賴它的模塊。這種隱式可讀性是通過在requires語句中包含public修飾符來表達的。
在一個模塊的公開API使用不可變列表的例子中,通過聲明公開依賴,模塊將可讀性授予Guava以及所有其他依賴Guava的模塊。
示例:隱式可讀性讓我們轉到UI模塊,它在其API中暴露了一個模型,而該模型使用了javafx.base模塊的類型。現在,我們可以修改模塊描述符,以便它可以公開聲明需要那個模塊:
module com.infoq.monitor.ui { // 將javafx.base暴露給依賴它的模塊 requires public javafx.base; requires javafx.controls; requires javafx.graphics; exports com.infoq.monitor.ui.launch; exports
com.infoq.monitor.ui.show; }
我們的主模塊現在可以隱式讀javafx.base,而不必顯式依賴它,因為它依賴的com.infoq.monitor.ui模塊已經輸出了它:
module com.infoq.monitor { requires com.infoq.monitor.stats; requires com.infoq.monitor.db; requires com.infoq.monitor.ui; // 我們不再需要javafx.base來更新UI模型了 requires java.logging; // 無輸出程序包
}雖然com.infoq.monitor讀com.infoq.monitor.stats的原因為此改變了,但這一事實沒有變。因此,這既沒有改變模塊圖,也沒有改變編譯和運行應用程式所需的命令。超出模塊邊界
再次引用設計概覽:
但是,我們可以將此運用到什麼程度呢?例如,考慮下通常,如果一個模塊輸出了一個程序包,而後者所包含的一個類型在簽名中引用了第二個模塊中的程序包,那麼第一個模塊的聲明中應該包含對第二個模塊的requires public依賴。這將確保其他依賴於第一個模塊的模塊自動地就可以讀第二個模塊,由此,就可以訪問那個模塊輸出程序包中的所有類型。
java.sql模塊。它暴露了Driver接口,其中包含一個public方法getParentLogger,返回值為aLogger。由於該模塊公開需要java.logging,所以任何使用Java的SQL特性的模塊也都可以隱式訪問日誌API。
考慮到這一點,讓我們再次看下資料庫模塊:
理論上講,需要java.logging的聲明是必要的,可能看上去多餘。那麼該刪掉它嗎?為了回答這個問題,我們必須看下com.infoq.monitor.db到底怎麼使用java.logging。我們的模塊也許只是為了能夠調用Driver.getParentLogger而讀它,然後使用logger(例如,記錄一條消息)做一些事情就夠了。在這種情況下,我們的代碼同java.logging的交互恰恰發生在它同java.sqlDriver交互緊鄰的地方。我們在前文中將其稱為com.infoq.monitor.db和java.sql的邊界。或者,我們可能在com.infoq.monitor.db中到處用到日誌記錄功能。那麼,java.logging中的類型會出現在許多與Driver無關的地方,也就不能再視為僅限於和java.sql的邊界。
由於Jigsaw是一項前沿技術,所以社區還有時間討論這個主題,就推薦實踐達成一致。我的觀點是,如果一個模塊不只是在同另一個模塊的邊界處使用,那麼就應該顯式地聲明需求。這種方法使系統結構更清晰易懂,同時也經得起未來重構時模塊聲明的考驗。因此,只要我們的資料庫模塊會獨立於SQL模塊使用日誌記錄,那麼我就該保留依賴聲明。
聚合模塊隱式可讀性為所謂的聚合模塊提供了可能,這種模塊自己不包含任何代碼,但為了方便使用聚合了若干其他模塊。Jigsaw JDK已經引入了這種機制,將「緊湊配置文件(compact profile)」 模塊化,而暴露的那些模塊恰恰就是程序包包含在配置文件中的模塊。
以我們的微服務monitor為例,我們可以設想成一個聚合了統計、用戶界面和數據模塊的API模塊,這樣,主模塊就只有一個依賴了:
module com.infoq.monitor.api { requires public com.infoq.monitor.stats; requires public com.infoq.monitor.db; requires public com.infoq.monitor.ui; //隱式可讀性是不可傳遞的 //因此我們必須顯式列出'javafx.base'
requires public javafx.base } module com.infoq.monitor { requires com.infoq.monitor.api; requires java.logging; //沒有輸出程序包 }
這理論上是有用的,但在這個簡單的例子中沒有提供特別的好處。
服務目前為止,我們已經探討了在編譯時確定和聲明的依賴。如果依賴採用服務的形式,一個或多個模塊提供一種功能,抽象成一個單獨的類型,而其他服務消費該類型的實例,那麼就可以實現鬆耦合。消費者可以使用模塊系統發現服務提供者。這就實現了服務定位器模式,模塊本身就扮演了定位器的角色。
一個服務就是一組提供一個整體功能的接口和(通常是抽象)類。它所包含的所有類型都必須能夠從一個單獨的類型(如一個接口)訪問,以便用其加載服務。
服務提供模塊包含一個服務的一個或多個實現。每個實現在模塊描述符中有一個provides X with Y;子句,其中X為服務接口的完全限定名,Y為實現類的完全限定名。Y要有一個public無參數構造函數,以便模塊系統能夠對它進行初始化。服務消費者模塊讀取服務模塊,其描述符中包含一個uses
X;子句。然後,它會在運行時調用ServiceLoader.load(Class)獲取一個服務接口加載器。該加載器是一個可疊代對象,包含了實現X以及由服務提供模塊提供的所有類的實例。
當像這裡描述的那樣使用服務時,不僅實現了編譯時鬆耦合(因為消費者沒有聲明對提供者的依賴),還實現了運行時鬆耦合,因為模塊系統不會創建從消費者到提供者的讀邊。
另外一個有趣的方面是,一個服務的可用提供者集合是由模塊路徑定義的(亦即通常在啟動時定義)。確切地說,那些提供服務實現的可見模塊會在運行時通過服務加載器獲取。因此,可以通過編輯系統模塊路徑並重啟來影響系統的行為。
模塊,我們已經討論過,該模塊用於連接運行在我們網絡中的服務,並生成統計信息。對於單個模塊而言,這聽上去工作太多,但我們可以把它分割。監視單個服務是一個標準任務,因此,我們為該任務創建一個API,並將其放進新模塊com.infoq.monitor.watch。該服務接口名為com.infoq.monitor.watch.Watcher。
現在,我們可以自由創建一個或多個實現具體微服務的模塊。我們將這些模塊命名為com.infoq.monitor.watch.login、com.infoq.monitor.watch.shipping等等。它們的模塊描述符如下:
module com.infoq.monitor.watch.login { // 該模塊需要定義它所提供的服務; // 該模塊具備隱式可讀性,因此本身就是可用的; requires public com.infoq.monitor.watch; provides com.infoq.monitor.watch.Watcher with
com.infoq.monitor.watch.login.LoginWatcher; }
請注意,它們只提供了Watcher的實現,但沒有輸出程序包。
提供的微服務,我們現在必須確保它仍然能夠使用那個功能,新的如下:module com.infoq.monitor.stats { requires java.logging; requires com.infoq.monitor.watch; // 我們必須聲明依賴哪個服務 uses com.infoq.monitor.watch.Watcher; exports
com.infoq.monitor.stats.get; }
現在,我們可以在其代碼中的某個地方使用如下代碼:
List<Watcher> watchers = new ArrayList<>; ServiceLoader.load(Watcher.class).forEach(watchers::add);
上述代碼會生成一個列表,其中包含了由模塊路徑上的模塊提供的每個Watcher實現的一個實例。從現在開始,代碼就跟以前一樣了,也就是連接服務,生成統計信息。
這裡我們僅看下com.infoq.monitor.stats的模塊圖(因為其他所有的部分都沒有變化),新版本的模塊圖如下:
(點擊放大圖像)
注意所有指向新模塊的箭頭;這是一個典型的依賴反轉原則示例。
編譯、打包和啟動都同以前一樣(除了現在模塊比以前多了)。
遷移截至目前,我們已經探討了將一個完整應用程式及其所有依賴轉換成模塊的場景。但是,當Jigsaw第一次從其全新的程序包中刪除時,那還不會很常見;大部分項目都會依賴一些尚不適合於模塊系統的庫,而且無法控制它們。
Jigsaw團隊已經直接解決了這個問題,提供了一種逐步向模塊化遷移的方法。為此,他們引入了兩種我們尚未討論的模塊。
模塊種類II我們已經了解了平台模塊和應用程式模塊。它們都完全了解模塊系統,模塊描述符是它們的一個關鍵特徵。由於它們有名字,我們稱它們為命名模塊。
對於不了解模塊系統的工件,還有其他兩種模塊。
在Jigsaw之前,從類路徑中加載的所有類型最終都處於同一個空間,在這個空間裡,它們彼此之間可以自由訪問。這個非結構化的空間還會繼續存在:每個類加載器會有一個唯一的未命名模塊,它會將從類路徑中加載的所有類型分配給該模塊使用。
未命名模塊可以讀取其他所有模塊,並輸出所有的程序包。由於模塊化不應該依賴於類路徑中的隨機內容,所以命名模塊不能require未命名模塊,因此也就無法讀取它們(不藉助反射的話)。
但是,沒有模塊描述符的工件仍然可以放在模塊路徑上。在這種情況下,模塊系統會為它創建一個全功能的模塊,我們稱之為自動模塊。
自動模塊的名稱是根據工件的文件名生成的,它可以讀取其他所有模塊,並輸出它們所有的全部程序包。由於模塊系統很容易在啟動時檢查一個特定的自動模塊是否在模塊路徑上,所以命名模塊可以依賴它們, 並因此能夠讀取它們。
因此,在常見的單個應用程式類加載器中,應用程式可見模塊空間由以下幾個部分構成:
這些類型的模塊開闢了逐步向模塊系統遷移的路徑。(然而,需要注意的是,模塊關係並不是唯一的障礙。)
前面已經講過,整個應用程式,包含它所有的依賴,都可以放在類路徑上。當出現一些妨礙遷移的問題時,這是一種重要的應對方法。
現在,我們看下這種方法為什麼有效:類路徑上的所有工件都會合成為一個未命名模塊,其中的所有類型彼此可以自由訪問。為了使用Java的公共API,它們必須訪問平台模塊,由於未命名模塊可以讀取其他所有的可見模塊,所以它們能夠做到這一點。
自下而上遷移從無依賴的工件開始,它們可以迅速模塊化。以此為基礎,其他項目可以遷移到Jigsaw。
如果項目已經遷移,那麼客戶可以將模塊化JAR文件放在模塊路徑上,並通過名字引用它們。即使沒有遷移,代碼仍然來自類路徑,那麼它也可以訪問已遷移的工件,因為未命名模塊可以讀取其他所有模塊。或者,客戶可以決定將模塊化JAR文件放在類路徑上。
這種方法最適合那些依賴少、依賴關係完善的庫項目。但是,隨著依賴數量的增加,項目可能不希望等著所有依賴模塊化。對於大型Java應用程式而言尤其如此,它們可能會更喜歡採用另一種方法。
自上而下遷移從為項目的所有工件創建模塊描述符開始。它們需要有一個名字,並且必須指定它們依賴哪些其他的內部工件以及它們需要輸出的程序包。
當然,這個過程會遇到外部依賴。如果有可用的適合Jigsaw的版本存在,那最好。如果不存在,那麼就是要採用自動模塊的方式:項目工件require名稱由Jigsaw根據工件文件名生成的模塊,而工件會放在模塊路徑上。
對於直接依賴,這樣做就夠了,新應用程式的模塊已經可以訪問它們了。這些依賴可能會引入傳遞性依賴。但是,由於直接依賴已經轉換成了自動模塊,後者可以讀取包括未命名模塊在內的其他所有模塊,所以它們的依賴可以放在類路徑上。
對於大型項目,這種手動方法就不合用了,需要藉助構建工具。Gradle和Maven已經開始研發同Jigsaw相關的特性。
要了解更多有關遷移的細節,可以查看Alex Buckley和Alan Bateman在JavaOne大會上的所作的題為「高級模塊化開發」的演講,這兩位都是Oracle Jigsaw團隊的成員。
示例:遷移依賴比方說,我們的資料庫模型使用了Guava,在路徑libs下,我們有一個工件guava-19.0.jar。我們不能簡單地把它放在類路徑上,因為我們的應用程式已經恰當地模塊化了。
Java可以根據文件名guava-19.0.jar生成模塊名guava。了解到這一點,我們就可以更新資料庫模塊的描述符:
module com.infoq.monitor.db { requires java.logging; requires java.sql; requires guava; exports com.infoq.monitor.db.write; }javac \ -mp libs \ -d classes/com.infoq.monitor.db \
${list of source files}打包過程沒有變化。如果我們像以前一樣啟動我們的應用程式,那麼JVM會報告無法找到模塊guava。要解決這個問題,需要將目錄libs添加到模塊路徑上:下一步
我們已經探討了Jigsaw的基本示例,並看到了它所提供的核心特性。除了等待Java 9的到來之外,我們其他還能幹些的什麼呢?
深入學無止境,下面是我們在討論中沒有涉及的兩個高級主題:
「模塊系統的現狀」這篇優秀的文章展示了如何將模塊結合反射一起使用,其中包括在運行時增加讀邊、層的新概念以及同類加載器交互。
新工具jlink可以用於創建僅包含一個平台模塊特定集合的運行時鏡像;這在Jigsaw快速入門指南中有介紹,我在此強烈推薦。
此外,Jigsaw團隊在JavaOne 2015和Devoxx BE 2015大會上的演講也涵蓋了這些主題。我以前在這裡進行過匯總。
觀察有關Jigsaw的所有內容都可以在該項目的OpenJDK網站上找到。有關Jigsaw項目的最新消息,可以從Jigsaw-Dev郵件列表了解到。我也會繼續在我的博客上探討這個主題。
準備前面已經提到,遷移到Jigsaw有點複雜。在項目準備的過程中,我們應該檢查下它們是否依賴Java 9沒有提供或已經刪除的東西。
可以使用Java依賴分析工具jdeps分析內部API依賴這個重大的障礙(部分內部程序包介紹、針對Windows和Unix平台的官方文檔),該工具在JDK 8中已經提供。另外,至少有三種面向Maven的jdeps-plugins,分別由Apache、Philippe Marschall和我提供。後者允許項目逐步移除對內部API的依賴,而且能防止故態復萌。
如果你擔心Java 9不提供某些特定的API,那麼你可以查看相應的OpenJDK項目郵件列表,因為他們會負責開發這些API的公共版本。
此外,我們應該明確項目中的關鍵依賴,並同那些團隊核實一下,他們如何為Java 9做準備。
應用Jigsaw早期訪問版本已經提供下載,可以用於試驗性地編譯和運行現有項目。遺憾的是,構建系統支持尚不完善,但相關工作正在推進中。
通過這種方式收集的信息和問題可以通過發布到Jigsaw-Dev郵件列表來向項目組反饋。下面這段話引自一個被大量提及的JEP的(差不多是)結束語:
從理論上確定這些變化的全部影響是不可能的。因此,我們必須依賴大量的內部、尤其是外部測試。[……]如果對於開發人員、部署人員和終端用戶而言,這些變化中的一部分是無法克服的,那麼我們將研究減輕影響的方法。
此外,還有一個全球性的Java用戶組AdoptOpenJDK,對於早期採用者而言,這是一個不錯的聯繫方式。
Nicolai Parlog是一名軟體開發人員,同時也是一名Java愛好者。他不斷地進行有關Java的閱讀、思考和寫作。編碼既是他的謀生方式,也是他的興趣所在。他是多個開源項目的長尾貢獻者,並在CodeFX發表有關軟體開發的博客。讀者可以在Twitter上關注Nicolai。查看英文原文:
Programming with modularity and Project Jigsaw. A Tutorial Using the Latest Early Access Build您需要註冊一個InfoQ帳號或者登錄才能進行評論。在您完成註冊後還需要進行一些設置。