Java 模組化系統初探

ImportNew發表於2015-09-28

Java 模組化系統自提出以來經歷了很長的時間,直到 2014 年晚些時候才最終以 JSR(JSR-376) 定稿,而且這個部分有可能在 Java 9 中出現。但是一直以來都沒有可以使用的原型。9 月 11 日,OpenJDK 釋出的早期構建版本終於包含了 Jigsaw 專案。

昨天,我和同事 Paul Bakker 在 JavaZone 上對於 Java 模組化系統進行了討論。整個討論都建立在JSR-376 需求文件以及身邊一些珍貴的資訊上。在年初提出舉行這個報告的時候,我們曾深信不疑地認為在這個會上我們能夠展示一個原型,但是事情卻沒有按預想的那樣發展。現在的情況是,這個原型將在我們的報告結束之後釋出。這也意味著,報告中的一些內容已經有點過時了,但是主要的思想還是很有新意的。如果你對 Java 模組化系統方案一無所知的話,建議你在閱讀這篇文章之前先去看一下我們的報告。我們的報告介紹了現在的方案,並進一步與 OSGi 進行了比較。

為什麼要使用模組?

什麼是模組?我們為什又需要它們?如果希望有一個深入的討論,請閱讀“State of the module system”或者看一下我們的報告。對這塊還不是很瞭解的人來說,這裡有Cliff 的註釋版本。

我們都知道 Java 有 jar 檔案。但是,事實上這些都只是包含一些class(類)的壓縮檔案,這些 jar 包內部都是一些 package (包)。當你利用一些不同的 jar 包來執行應用程式的時候(複雜一點的程式也適用),你需要把它們放到指定的類路徑中。然後默默祈禱。因為沒有有效的工具來幫助你知道,你是否已經把應用所需要的 jar 包都放入類路徑中了。或者有可能你在不經意間將同樣的類檔案(在不同的 jar 包中)都放入了類路徑中。類路徑災難(類似於 DLL 災難)是真實存在的。這會導致執行時出現糟糕的狀況。同時,在執行時我們也無法得知 jar 中包含哪些類。從 JRE 角度來說只知道有一堆類檔案。事實上 jar 包之間是相互依賴的,但目前還不能把這種依賴關係記錄到資料檔案中去。理想的情況是,你可以隱藏 jar 包中類檔案具體的實現,只是提供一些公共的 API 。在 Java 中提出模組化系統就是為了解決這些問題的:

  • 模組成為首先要考慮的部分,它能夠分裝實現細節並且只暴露需要的介面。
  • 模組準確地描述了他們能夠提供的介面,以及他們的需要部分(依賴)。由此,我們可以在開發的過程中弄清和處理依賴關係。

模組系統極大地提升了大型系統的可維護性、可靠性、安全性。至少 JDK 本身還缺少這樣的系統。通過這樣的模組系統,模組圖能夠自動地構建。這個圖只包括了你的應用程式執行時所須要的模組。

安裝 JDK9 預覽版

如果你想親自嘗試編寫示例程式碼,你需要安裝包含 Jigsaw 原型的 JDK9 早期構建版本。在 OSX 上,你需要解壓文件,然後把解壓出來的目錄移動到 Library/Java/JavaVirtualMachines/ 下。然後你需要設定環境變數,將 JAVA_HOME 環境變數指向 JDK9 的目錄。我使用了非常好用的setjdk 指令碼,通過它可以在命令視窗中實現 Java 安裝的命令切換。你很有可能不願意使用這個早期構建版本作為你的 Java 安裝版本。你可以通過 java -version 來確認安裝完成。輸出如下面所示:

java version "1.9.0-ea"
Java(TM) SE Runtime Environment (build 1.9.0-ea-jigsaw-nightly-h3337-20150908-b80)
Java HotSpot(TM) 64-Bit Server VM (build 1.9.0-ea-jigsaw-nightly-h3337-20150908-b80, mixed mode)

只要輸出中包含 Jigsaw ,你就可以繼續了。文章後面的示例程式碼可以去 https://github.com/sandermak/jigsaw-firstlook 下載。

一個簡單的例子

你仍舊可以通過類、jar包以及類路徑這樣“傳統方式”的方式來使用 JDK9 。但是明顯地我們想要採用模組的方式。所以我們將建立一個包含兩個模組的工程:模組一使用了模組二中的程式碼。

首先要做的就是,構建我們的工程並把兩個模組很好地區分開來。然後,模組中需要以 module-info.java 檔案的形式新增後設資料。我們的示例構建如下:

src
  module1
     module-info.java
     comtestTestClassModule1.java
  module2
     module-info.java
     commoretestTestClassModule2.java

接著,我們將介紹 package (包)層最頂上的一層(module1、 module2),這部分你在之前已經構建好了。在這些“模組目錄”中,可以看到 module-info.java 檔案在根目錄下。此外請注意,這兩個類都是在顯示命名的包中的。

請看 TestClassModule1 的程式碼:

package com.test;

import com.moretest.TestClassModule2;

public class TestClassModule1 {

   public static void main(String[] args) {

     System.out.println("Hi from " + TestClassModule2.msg());

   }

}

看起來很普通對吧?這裡並沒有涉及模組,而是匯入了 TestClassModule2 ,主函式之後會去呼叫其中的 msg() 方法。

package com.moretest;

public class TestClassModule2 {

   public static String msg() {

     return "from module 2!";

   }

}

到目前為止,module-info.java 還是空的。

對 Java 模組進行編譯

現在進行下一步:編譯我們的模組,並關聯原始檔。為了做這項工作,我們將介紹一個新的 javac 編譯引數:

javac -modulesourcepath src -d mods $(find src -name '*.java')

使用上面語句時,我們假設命令程式已經處於 src 資料夾的上級目錄中了。-modulesourcepath 引數會讓 javac 從傳統編譯模式進入模組模式。-d 標記指出了編譯好的模組的輸出目錄。javac 將以非打包檔案的形式輸出這些模組。如果我們這之後想以 jars 的形式使用這些模組的話,需要一個單獨的步驟。

那麼當我們呼叫上面的 javac 命令列的時候會發生什麼那?編譯出錯了!

src/module1/module-info.java:1: error: expected 'module'
src/module2/module-info.java:1: error: expected 'module'

空的 module-info.java 檔案導致了這個錯誤。所以,一些新的關鍵字將被引入到這些檔案中來,這些都是模組中非常重要的部分。這些關鍵字的作用域就是 module-info.java 的定義部分。你還可以在 java 的原始檔中使用 module 型別的變數。

我們採用了最少的描述資訊,並更新了模組描述檔案:

module module1 { }

然後是模組2:

module module2{ }

現在,模組已經被準確地命名了,但是還沒有包含其它的資料。再次編譯會導致新的錯誤:

src/module1/com/test/TestClassModule1.java:3: error: TestClassModule2 is not visible because package com.moretest is not visible

封裝出現了!預設情況下,模組內部的類或者其他型別對外都是隱藏的。這就是 javac 不允許使用 TestClassModule2 的原因,即使它是一個公共的類。如果我們還是使用基於傳統類路徑的編譯的話,一切都可以正常運作。當然我們也可以通過明確地將 TestClassModule2 暴露給外部來解決這個問題。接下來的這些改變對於 module2 中的 module-info.java 來說是必須的:

module module2 {

  exports com.moretest;

}

這還不夠。如果你將修改後的編譯,你會得到同樣的錯誤。那是因為,雖然現在 module2 已經暴露了所需的包(包含所有的公共型別),但是 module1 還沒有宣告它對 module2 的依賴。我們同樣可以改變 module1 的 module-info.java 檔案來解決這個問題:

module module1 {

   requires module2;

}

通過指定名字的方法可以表示對其它模組的依賴,儘管在這些模組中是以包的形式匯出的。這方面還有很多可以說的東西,但是我並不想在初步的介紹中涉及。在做完這一步之後,我們使用 Jigsaw 第一次成功編譯了多模組專案。如果你開啟 /mods 目錄,你能看到編譯出來的東西被整齊地劃分為兩個目錄。這就成功了!

執行模組化程式碼

只是編譯的話並沒有多大樂趣。我們希望應用程式能夠執行起來。幸運的是,JRE 和 JDK 已經在這個原型中支援模組關聯。這個應用可以通過指定模組路徑的方式來啟動,而不是類路徑:

java -mp mods -m module1/com.test.TestClassModule1

我們把模組路徑指向 mods 資料夾,這個檔案就是 javac 編譯時寫輸出模組的地方。而 -m 指出了最初要啟動的模組,通過這個模組可以逐步啟動其他模組。我們同樣新增了在初始化時需要呼叫的啟動類的名字,執行結果如下所示:

Hi from from module 2!

未來

這部分介紹可以讓你初步瞭解可以使用 Java 9 中的模組可以做什麼。這部分還是需要更多的探索。就像打包一樣:除了jar包,即將會有一種新的形式叫做 jmod 。這個模組化系統同樣包括一個服務層,它可以通過介面繫結服務提供者和服務使用者。可以把這個看成反轉控制:模組系統擔任服務註冊管理的角色。還有一個值得期待的地方是,JDK 本身將會如何使用模組化系統進行模組化。這有可能支援一些非常棒的技術,比如建立一個執行時映象,這個映象可以只包括 JDK 和你應用所需要的那些模組。好處有:佔用更少的空間,對於程式整體的優化可以有更多的選擇等等。這些前景都是很光明的。

我接下來將嘗試移植一個簡單的 OSGi 應用程式(該程式會使用一些模組和服務)到 Java 9 模組系統上。敬請關注!

相關文章