服務模塊化
服務模塊化踐行
2017 年 9 月 jdk 9 正式發佈,帶來了很多新特性,其中之一便是模塊化,JDK 模塊化的前身是項目 Jigsaw,於 2008 開始孵化,最早計劃用於 jdk7,一部分內容推遲到了 jdk8,實際上在 jdk9 才完成了該項目全部目標,即實現一個模塊系統,並以此實現 jdk 自身模塊化。本文主要闡述模塊化的概念,爲什麼關注模塊化,基於 jdk9 的模塊化實現原理和項目實踐。
1. 什麼是模塊化
模塊化是個廣泛的概念,用於軟件編程就是將系統分解成獨立且互相連接的模塊的行爲,拆分的模塊通常需要提前定義好標準化的接口,以便讓各模塊獨立開發情況下,還能互相調用不受影響。實際上在面嚮對象語言中對象之間的關注點分離與模塊化的概念基本一致,在實際應用開發中,將複雜業務系統按照業務邏輯等分割成多個獨立的模塊,各模塊提前定義好對外的服務接口,各模塊獨立開發,根據依賴的模塊可獨立完成業務模塊測試、交付。Java 語言並不是按照模塊化思想設計的(除了 package,在 Java 語言和虛擬機規範各版本第 7 章 package,程序被組織爲一組包。包的成員是類、接口以及子包,它們以包爲編譯單元聲明)但是 java 社區早就有很多模塊。一個 jar,一個包,任何一個 java 類庫,實際上都是一個模塊,通常模塊都附帶一個版本號,以便模塊升級提供新功能並不對低版本的模塊產生影響。
2. 爲什麼模塊化
模塊化有助於將應用分解爲不同的模塊,各個模塊可以單獨測試、開發、交付。類庫基本上都是模塊,如果你想將部分類庫提供給別人使用或者使用了別人提供的類庫,那麼實際上你已經參與過模塊化應用了。在實際項目中,一般使用構建工具(maven、gradle 等)組建,明確指明瞭依賴的類庫,以及變成類庫,供他人使用。
模塊化的好處之一是便於模塊獨立測試、開發、交付。模塊可按照業務核心情況或依賴順序部分交付,以便項目逐步完成交付,節省資源,增加迭代優化空間,這個概念提別像敏捷開發,採用迭代、循序漸進的方法進行軟件開發,把一個大項目分爲多個相互聯繫,但也可獨立運行的小項目,並分別完成,在此過程中軟件一直處於可使用狀態。
模塊化的另一個好處是便於升級,修復 bug 並提供新的服務,而版本號的存在就是爲了區分模塊的歷史版本以及避免依賴發生錯誤。像 guava、fastjson 和 fastjson2 等類庫證實了這點。
模塊化也可給項目管理帶來方便,複雜業務分割成一個個獨立可複用的模塊,項目結構性更好,出現問題或者需要部分優化,只需要關注部分模塊,對於依賴的模塊由其他人提供維護即可,減少了維護和關注的成本。
3. 模塊化的原理
首先需要安裝 jdk9,下載地址放在文末附錄。
如下圖 1 所示爲安裝好的 jdk9,圖 2 所示爲 jdk8 的目錄,是多個 jar。
以上圖 1 和圖 2 對比可以看到 jdk9 拆分成了具體模塊,不再是一個個的 jar,每個模塊都有一個 module-info.class,文件定義模塊的名字、依賴的模塊、對外開放的類、接口實現類等,實際上 module-info 就是是模塊化的聲明文件。
除了組織形式發生變化外,真正的區別在哪裏呢?圖 3 是 jdk.internal.loader.BuiltinClassLoader 的 loadClassOrNull 方法中的代碼片段,是進行類加載的方法,代碼展示先查找 LoadedModule (模塊信息)如果有的話就進行類加載,否則的話,按照雙親委派模式向上委託進行類加載,後一步是爲了向前兼容,前一步就是模塊化實現的核心原理,類加載機制不再向上委託,而是根據 LoadedModule 限制類加載。
其初始化在 java.lang.System# initPhase2 如圖 3.1,主要是虛擬機進行系統模塊化的初始化,並返回 ModuleLayer,稱爲 layer(層,表示一組類加載器),有兩種層,虛擬機提供的 boot layer 和用戶自定義的 layer,用於將基礎模塊和用戶定義模塊與類加載器(層)關聯。
圖 3
模塊的定義在 Module#defineModules,詳細的解釋可在 java9se 虛擬機規範 5.3.6 找到,Java 虛擬機支持將類和接口組織成模塊,調用 defineModules,將模塊與 layer(類加載器)關聯,設置模塊可訪問、開放的資源以及依賴的資源(由此限制模塊的訪問), 訪問控制由類的運行時模塊管理,不是由創建類的類加載器或類加載器服務的層管理,至此模塊化的初始化和限制訪問核心功能實現。也可按照以下代碼理解模塊化的組織和實現。BuiltinClassLoader 的實現類有三個 AppClassLoader,BootClassLoader,PlatformClassLoader,jdk9 的類加載器。
//初始化 layer
ModuleLayer boot = ModuleLayer.boot();
Configuration configuration = boot.configuration();
//獲取解析的模塊
Set<ResolvedModule> modules = configuration.modules();
modules.forEach(resolvedModule -> {
//獲取模塊句柄
ModuleReference reference = resolvedModule.reference();
//模塊化的名稱
System.out.println(reference.descriptor().name());
try (ModuleReader reader = reference.open()) {
//模塊化下的全部資源
reader.list().forEach(System.out::println);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
});
jdk9 以前的類加載機制是大家熟識的雙親委派三層模型,bootstrap classloader <-- extension classloader <-- application classloader,這裏不在贅述。下面展示 jdk9 帶來的改變,維持了三層模型,爲了向前兼容,自 JEP 220.extension classloader 變改爲 platform classloader,與 application classloader 不在是 URLClassLoader 的實現,而是其內部存有 LoadedModule,並優先根據模塊化信息自我進行類加載,否則委託給父類,而 platform classloader 還可以委託給 application classloader ,實際的加載機制如下圖 4 所示,模塊化的類加載機制打破了雙親委派,效率更加高效。以上便是模塊化實現的核心原理,Module 控制模塊下類和接口的訪問性,模塊化的類加載不再是雙親委派,運行時模塊根據模塊之間的關係,與 layer(一組類加載器)關聯,按照下圖方式進行類加載。
圖 4
4. 模塊化踐行
下面實踐基於 jdk9 模塊化項目編譯到運行全過程目錄 4.1 以及完整多模塊化的項目的使用 4.2
4.1 模塊化項目
由 hello 項目入手品略模塊化項目的編譯、打包、運行、生成運行時環境的過程,深入理解模塊化的按需打包的優點。着重展示模塊化項目從建立到可運行環境輸出過程,項目名爲 hello,項目目錄如下圖 5:
src 目錄下新建一個 module-info.java,模塊名是 hello。在 hello 目錄下,新建 Main.java,添加代碼代碼,其實就是打印一個 hello world。下面進行編譯,運行,鏡像輸出。
public static void main(String[] args) {
System.out.println("hello world");
}
► 4.1.1 編譯
編譯 java 文件,out 是個目錄,編譯生成文件到 out 這個目錄下:
javac -d out .\src\hello\Main.java .\src\module-info.java
►4.1.2 打包
將 out 目錄下全部文件也就是(*)打包成 hello.jar 文件,存放在 jar 目錄下,並指定應用程序入口點爲 hello.Main,-c 創建新檔案,-f 指定檔案文件名,-e 指定應用程序入口點。
cd .\out\
mkdir jar
jar -cfe hello.jar hello.Main *
►4.1.3 運行
運行生成的 jar ,--module-path 指定模塊路徑, jar 是存放 hello.jar 文件的目錄,控制檯輸出 hello world
java --module-path .\jar\ --module hello/hello.Main
或者
java --module-path .\jar\ --module hello
►4.1.4 生成模塊
指定生成模塊的 jar 是 hello.jar,生成模塊 hello.jmod
jmod create --class-path hello.jar hello.jmod
►4.1.5 生成運行環境
將 hello.jmod 放到 jdk 安裝目錄下的 jmods 目錄下(windows 下 module-path 指定多個路徑分隔符是半角分號【;】,Linux 分隔符是半角冒號【:】我的環境是 windows,嘗試多次均爲未成功,所以粘貼這個模塊到 JDK 的基礎模塊中,指定 module-path 爲當前目錄即可)並在此目錄執行以下命令,指定模塊路徑爲當前目錄,--add-modules 添加 java.base 和 hello 模塊 ,--launcher 定義一個入口點直接運行模塊 --output 指定生成的運行時環境的目錄名稱。
jlink --module-path . --add-modules java.base,hello --launcher hello=hello --output jre/
►4.1.6 運行
打開 jre 目錄,可以看到如圖 6 所示,bin 目錄下生成可運行 hello 和 hello.bat,windows 下命令行運行 .\hello.bat,控制檯打印,hello world
►4.1.7 小結
以上項目生成的文件是一個完整的可運行的 Java 運行環境即 Java Runtime Environment 即 jre,而這個可運行的環境大小隻有 35.9 MB,完整的 jre 是 215M(我的環境中),這也就是模塊化的一大優點,可按需打包依賴,從 jdk 層支持,應用依賴也可以按照如此按需打包,減少浪費資源,以上是模塊化從編譯到生成 jre 的過程,下面我們進行模塊化的完整項目開發。
4.2 多模塊項目實踐
一個完整項目如何模塊化?模塊之間如何依賴使用?怎麼對外開放服務?如何對外允許反射的服務和以及隱式的依賴傳遞,下面項目深入展示模塊化的項目使用基本要點。着重展示了模塊化的使用以各關鍵字的詳細解釋。
假設場景是每天的生活,新建一個項目,建四個模塊,eat、transportation、work、console 項目如下,eat 模塊模擬喫喝,transportation 模塊模擬交通,work 模塊模擬工作,console 模塊模擬生活,項目目錄如圖 7 所示。
►4.2.1eat 模塊
eatapi 目錄下,對外提供服務接口,喫飯喝水兩個方法,
public interface EatApi {
void eat();
void drink();
}
eatservice 目錄下,實現 EatApi 接口,
public class EatApiImpl implements EatApi {
@Override
public void eat() {
System.out.println("喫飯了");
}
@Override
public void drink() {
System.out.println("喝水了");
}
}
模塊化 module-info 類,定義名稱爲 eat,exports 對外暴露 eatapi 接口,接口的實現爲 EatApiImpl 類,provides with 可被 ServiceLoader 根據 SPI 的方式加載到,但是反射並不能獲取實現類。
module eat {
exports eatapi;
provides eatapi.EatApi with eatservice.EatApiImpl;
}
►4.2.2.transportation 模塊
transportapi 目錄下,對外提供服務,模擬交通,
public interface Transportation {
void transport();
}
transportservice 目錄下,實現 transportapi 接口
public class TransportationImpl implements Transportation {
@Override
public void transport() {
System.out.println("開車出去");
}
}
模塊化 module-info 類,定義名稱爲 transportation,exports 對外暴露 transportapi 接口,接口的實現爲 TransportationImpl 類,opens 關鍵字,可以加在 module 關鍵字之前,表明整個模塊都可以被深度反射,opens transportservice 只表明該包下的類可以被深度反射。
module transportation {
exports transportapi;
provides transportapi.Transportation with transportservice.TransportationImpl;
opens transportservice;
}
►4.2.3.work 模塊
workapi 目錄下,對外提供服務,模擬工作,
public interface Work {
void work() throws Exception;
}
workservice 目錄下,實現接口,通過 ServiceLoader 獲取 eat 模塊 EatApi,通過反射獲取 Transportation 實現了類。
public class WorkImpl implements Work {
@Override
public void work() throws Exception {
System.out.println("開始工作了");
//獲取服務
EatApi eatApi = ServiceLoader.load(EatApi.class).findFirst().get();
//喝口水
eatApi.drink();
//反射獲取 Transportation實現了類
Transportation transportation = getTransportation();
//出去一趟
transportation.transport();
//喫點東西
eatApi.eat();
//喝口水
eatApi.drink();
}
private Transportation getTransportation() throws Exception{
Class<Transportation> transportationClass = (Class<Transportation>) Class.forName("transportservice.TransportationImpl");
Transportation transportation = transportationClass.getDeclaredConstructor().newInstance();
return transportation;
}
}
模塊化 module,workapi 可對外暴露,實現類是 WorkImpl,requires 表示依賴模塊, 依賴模塊 eat、transportation,調用了這兩個模塊的服務,transitive 關鍵字表示該依賴會被傳遞,引用本服務的服務也會引用 transitive 修飾的模塊,不用在主服務中在引一次,uses 表示使用模塊中的具體服務。
module work {
exports workapi;
provides workapi.Work with workservice.WorkImpl;
requires transitive eat;
requires transitive transportation;
uses eatapi.EatApi;
}
►4.2.4.console 模塊
該模塊調用 work 模塊以及 work transitive 的模塊,
模塊化配置如下,依賴模塊 work,使用 workapi.Work 和 eatapi.EatApi
module console {
requires work;
uses workapi.Work;
uses eatapi.EatApi;
}
day1 目錄下新建 Main,模塊 Work 的依賴隱式傳遞,最終打印出結果如圖 8 所示。
public class Main {
public static void main(String[] args) throws Exception {
//獲取work 服務
ServiceLoader<Work> load = ServiceLoader.load(Work.class);
Work work = load.findFirst().get();
//調用
work.work();
//其他服務
ServiceLoader<EatApi> eatLoader = ServiceLoader.load(EatApi.class);
EatApi eatApi = eatLoader.findFirst().get();
eatApi.eat();
eatApi.drink();
//反射獲取
Transportation transportation = getTransportation();
transportation.transport();
}
private static Transportation getTransportation() throws Exception {
Class<Transportation> transportationClass = (Class<Transportation>) Class.forName("transportservice.TransportationImpl");
Transportation transportation = transportationClass.getDeclaredConstructor().newInstance();
return transportation;
}
}
5. 總結
以上便是使用模塊化生成需要 jre 環境和在項目中使用多模塊服務的踐行。
模塊化核心原則模塊必須強封裝性,隱藏部分代碼,只對外提供指定服務,也就需要良好的接口定義並且顯示依賴,聲明式的服務依賴,不是使用了但不知道依賴來自哪裏的糊塗賬。可以提高模塊的可讀性,明確服務的入口和依賴,減少服務循環依賴,按需打包,解決反射帶來的全可見危害,提高安全性。但是就目前而言模塊化帶來的收益遠低於遷移工作,目前大家都在用 spring 的全家桶應用項目,使用很方便,但是真正按照模塊化將其切分出來,並且能夠完全理清楚項目依賴,也是有一定門檻的,不過模塊化的方法和工具,jdk 已然提供,模塊化的思維和想法是很值得學習的,相信在不久的將來,模塊化會更智能和完善。
6. 附錄
[1] 項目 hello https://gitee.com/lifutian66/java9/tree/master/hello
[2] 項目 java9 https://gitee.com/lifutian66/java9/tree/master/java9
[3] 生成 hello.jmod https://gitee.com/lifutian66/java9/hello.jmod
[4] 生成 jre https://gitee.com/lifutian66/java9/tree/master/jre
[5]jdk9 地址:https://www.oracle.com/java/technologies/javase/javase9-archive-downloads.html
[6]Modular Java: What Is It?https://www.infoq.com/articles/modular-java-what-is-it/
[7] 參考文檔:java9 模塊化開發核心原則和實踐
作者簡介
李福田
主機廠技術部 - 數科技術團隊。
2022 年加入汽車之家,目前任職於數科品牌私享家後端技術團隊,主要負責品牌私享家後端相關業務技術開發。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/rwzO1DSLfJAxuhR-5EIGFQ