Java 9 模塊化
Java 9 引入模塊(JPMS,Java Platform Module System), 其是在包上增加了新的抽象級別。本文主要介紹JPMS並討論它的多個方面內容。
1. 模塊概述
首先,在學習如何使用模塊之前需先理解模塊是什麼?模塊是一組緊密相關的包、資源以及模塊描述文件。也就是說,它是"Java包的包"抽象,增強代碼可重用性。
模塊中的包與Java誕生以來一直使用的包是相同的。當我們創建模塊時,我們在包中組織代碼,就像以前對所做的方式一樣。除了組織代碼外,還使用包來確定哪些代碼可以在模塊外部公開訪問,在本文後面將花更多篇幅討論該問題。
每個模塊負責其資源,如媒體或配置文件。之前資源放在項目根級別,我們手動管理資源屬於不同應用的部分。
創建模塊時,需要包括模塊描述文件,其中定義幾個方面的內容:
- Name – 模塊名稱
Dependencies – 該模塊依賴其他模塊列表
Public Packages – 允許模塊外能夠訪問的公共包列表
Services Offered – 提供給被其他模塊消費的服務實現
Services Consumed – 允許當前模塊成爲一個服務消費者
Reflection Permissions – 明確允許其他類使用反射訪問包的私有成員
模塊命名規則類似於包名稱(可以使用點,不能使用破折號),通常也是項目命名凡是(my.module)或DNS的反向形式(com.baidu.mudule)。
我們需要列出所有想公開的包,默認情況下所有包是模塊私有的。對於反射也是如此。默認情況下,我們不能對從另一個模塊導入的類使用反射。在本文的後面,我們將查看如何使用模塊描述符文件的示例。
- 模塊類型
新引入的模塊系統有四種模塊:
系統模塊:運行list-modules命令返回的模塊,包括java SE 和 JDK模塊。
應用程序模塊:當我們決定使用模塊時,我們通常希望構建這些模塊。一般在編譯的module-info.class文件中命名、定義,包括在Jar文件中。
自動模塊:通過增加已存在jar至模塊路徑引入非官方模塊。模塊的名稱將從JAR的名稱派生出來,自動模塊將對路徑加載的所有其他模塊具有完全的讀訪問權。
非命名模塊:當類或jar加載到類路徑,不是模塊路徑,則會自動增加非命名模塊。它是籠統全面的模塊,用於維護與之前Java代碼,保持向後兼容性。
- 模塊分發
模塊可以通過兩種方式分發:jar包形式或“暴露的”編譯項目,當然該項目與其他Java項目一樣,沒有什麼新奇。
我們可以創建有多個模塊項目組成一個主應用和幾個庫模塊。需要注意的是每個jar文件只能有一個模塊,因此準備構建文件時,我們需要確保綁定項目中每個模塊作爲一個獨立jar。
安裝Java9 時能看到JDK有了新的結構,已經將所有的原始包轉移到新的模塊系統中。輸入下面命令可以看到這些模塊:
java --list-modules
主要分爲四組: java, javafx, jdk, Oracle。java模塊是核心SE 語言規範實現類。javafx模塊是FX UI庫。jdk自身需要的在jdk模塊中,最後Oracle規範相關的在oracle模塊中。
2. 模塊聲明
創建模塊需要在包的根路徑下放一個特殊文件 module-info.java
,該文件稱爲模塊描述文件,包括所有必要的數據用於構建新模塊。
使用聲明構建模塊,主體內容可以爲空或有模塊指令組成:
module myModuleName {
// all directives are optional
}
使用關鍵字module
開始模塊聲明,後面跟上模塊名稱。這是模塊已經正常工作,但一般需要更多信息,即模塊指令。
2.1. Requires
第一個指令是 requires。用於聲明模塊依賴:
module my.module {
requires module.name;
}
現在my.module模塊有一個運行時和編譯時的依賴模塊:module.name。通過requires指令使得依賴模塊中所有公開暴露類型都可以被當前模塊訪問。
2.2. Requires Static
有時需要引用其他模塊,但實際並不會使用。舉例,可能需要寫一個工具函數用於當日志模塊存在時格式化輸出內部狀態,但並不是每個庫使用者都需要該功能,他們並不想增加額外的日誌模塊。這時需要使用可選依賴指令。通過可選依賴指令,可實現僅在編譯依賴:
module my.module {
requires static module.name;
}
2.3. Requires Transitive
通常使用庫使得編碼更加簡單。但我們需啊喲確保任何引用的模塊也同時引用其依賴的模塊,否則不能正常工作。幸運的是我們能使用 requires transitive
指令強制任何下游消費者也讀取必要的傳遞依賴:
module my.module {
requires transitive module.name;
}
現在當開發者引入 my.module
模塊時,無需再顯示聲明引入module.name
模塊。
2.4. Exports
缺省情況下,模塊不暴露任何API給其他模塊。這種強封裝機制是最初創建模塊系統的關鍵動機之一。主要我們代碼顯然更加安全,但現在需要顯示開放API給其他模塊。
可以使用exports指令暴露所有對應包下的public成員:
module my.module {
exports com.my.package.name;
}
現在當某人引入my.module
模塊時,則可以訪問 com.my.package.name
包下的所有public成員,但僅限於當前聲明的包。
2.5. Exports … To
我們能適用export指令開放public類給其他模塊,但有時不想讓所有人都能訪問我們的API。這時可以使用exports…to指令限制僅那個模塊可以訪問。
與exports指令類似,聲明公開一個包,但也能列出允許哪些模塊可以導入這個包。例如:
module my.module {
export com.my.package.name to com.specific.package;
}
2.6. Uses
服務(service)是可以被其他類消費特定接口或抽象類的實現,使用uses指令指定我們模塊消費的服務。
注意class.name也可以是服務接口、抽象類的名稱,不是實現類:
module my.module {
uses class.name;
}
這裏應該注意requires和users指令的區別。
我們可能使用requires指令引入了提供服務的模塊,但服務實現的接口來自某個傳遞依賴。這時無需強制我們模塊通過requires引入所有傳遞依賴,可以使用uses指令引入接口至模塊路徑。
2.7. Provides … With
模塊也作爲其他模塊消費的服務提供者(service provider)。首先使用provides關鍵字,然後是接口或抽象類名稱,接着使用with指令指定實現類名稱,即實現接口或繼承抽象類的類名稱。示例如下:
module my.module {
provides MyInterface with MyInterfaceImpl;
}
2.8. Open
前面提及設計模塊系統的動機爲封裝。Java9之前,可以使用反射檢查包中各種類型和成員,甚至是私有成員。這不是真正的封裝,這爲庫開發者帶來各種問題。Java9採用強制封裝,因此必須顯示授權給其他模塊對我們的類進行反射。如果需要繼續允許如Java老版本那樣的所有反射功能,需要簡單開放整個模塊:
open module my.module {
}
2.8. Opens
如果允許反射私有類型,但不是暴露所有代碼,可以使用opens指令直接暴露特定包。但注意,這樣即開放給整個世界,所以需要確認:
module my.module {
opens com.my.package;
}
2.9. Opens … To
有時反射是有必要的,但仍需要如封裝提供的安全性。我們可以有選擇性開放包給特定模塊,使用opens…to指令:
module my.module {
opens com.my.package to moduleOne, moduleTwo, etc.;
}
3. 可見性
這裏花點時間討論代碼可見性。很多庫依賴反射實現其魔法功能(JUnit和Spring),Java 9缺省情況下只能訪問導出包的public類、方法和字段。即使使用反射訪問非public成員並設置setAccessible(true),也不能成功訪問這些成員。
我們可以使用open, opens, and opens…to指令授權運行時反射訪問,注意這僅僅是在運行時。我們不能對私有類型進行編譯,而且永遠也不需要這樣做。
如果我們必須要編譯時訪問私有類型,並且也不是模塊的擁有者(即不能使用opens…to指令),那麼可以使用命令行–add-opens 選項允許自己模塊在運行時反射訪問鎖定的模塊。
這裏惟一需要注意的是需要訪問命令行參數,這些參數用於運行模塊以使其工作。
4. 總結
本文討論了Java 9 模塊系統的基本概念,模塊是什麼,模塊文件聲明,後續繼續提供一些實戰內容。