初探模組

May 19, 2022

Java SE 9 以後的重大特性之一是模組平臺系統,模組跟 Java 程式語言本身沒有關係,而是為了管理程式庫的功能封裝,以及管理程式庫間的相依性等需求而存在。

JVM 與 module-path

如果你基於 A 程式庫撰寫了新的程式庫,接著有同事想要使用你的程式庫,而你不想要他直接呼叫 A 程式庫的相關功能,以免他撰寫的程式直接依賴在 A 程式庫上,就目前你知道的知識來說,只要類別路徑上可以找得到 A 程式庫的相關類別,他就可以呼叫相關功能,日後程式庫之間錯綜複雜的相依性就從此開始了。

當然,Java 生態圈 20 幾年來,也為了這樣的問題提出了解決方案,也有第三方(Third-party)的模組系統,而為了要統一模組平臺的規格,以及為 Java SE 平臺瘦身(讓小型運算裝置可以依需求下載必要的模組而不是整個 JRE)、改進安全等因素,Java SE 9 決定納入模組平台系統成為標準之一。

那麼就來開始建立第一個模組吧!首先看看,一個未支援模組的程式專案要如何設定,才能使之成為模組。請複製範例檔資料夾 的 Hello2 資料夾至 C:\workspace,Hello2 中有個 src 資料夾,其中有 cc\openhome\Main.java 檔案:

package cc.openhome;

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, World");
    }
}

可以使用底下的指令來編譯並執行程式:

javac -d classes src/cc/openhome/Main.java
java -cp classes cc.openhome.Main

這是基於類別路徑的方式,若想要設定它為模組,第一件事是決定模組名稱,這邊假設模組名稱為 cc.openhome, 建議建立一個與模組名稱相同的資料夾,然後其中依套件設定的階層放入原始碼,在這邊可以在 src 中建立 cc.openhome 資料夾,然後將原先 src 中的 cc 資料夾放到 cc.openhome 資料夾:

初探模組

接下來,在 cc.openhome 資料夾建立 module-info.java 並撰寫底下內容:

module cc.openhome {}

這麼一來,在原始碼層面上,你就建立了第一個模組了,雖然 module-info.java 的副檔名為 .java,它實際上只是個設定檔,其中的 module 關鍵字僅在這個設定檔中進行設定之用,不是 Java 程式語言的一部份,副檔名為 .java,單純只是為了相容性,讓 javac 等工具程式易於處理這個設定檔罷了。

module 關鍵字定義了模組名稱為 cc.openhome,除此之外 沒有其他設定,這表示目前只能存取同一模組以及 Java 標準 API 的 java.base 模組,java.base 模組包含了像是 java.lang 等常用的套件;言下之意也表示,日後必要的話,可以在 module-info.java 中設定自己的模組可以公開哪些 API,或者是依賴在哪個模組之上。

那麼來編譯程式碼吧!可以將編譯出來的類別放在 mods 資料夾中對應模組名稱的資料夾之中:

C:\workspace\Hello2>javac -d mods/cc.openhome src/cc.openhome/module-info.java

C:\workspace\Hello2>javac -d mods/cc.openhome src/cc.openhome/cc/openhome/Main.java

C:\workspace\Hello2>

mods 中的 cc.openhome 就是完成編譯的模組,其他開發者若要使用此模組,可以在執行 java 時,透過 --module-path(或縮寫 -p)指定模組路徑。

由於 Main.java 撰寫了程式進入點,如果想執行它的話,可以透過 --module 或縮寫 -m 指定模組的程式進入點,例如:

C:\workspace\Hello2>java --module-path mods -m cc.openhome/cc.openhome.Main
Hello, World

在完全吻合名稱前要指定模組名稱,然而,這僅僅只是工具層面的需求,在程式碼撰寫上,使用模組的 API,不用進行任何變更。

cc.openhome.Main 也可以基於類別路徑來使用,方式與先前小節的說明是相同的,例如:

C:\workspace\Hello2>java -cp mods\cc.openhome cc.openhome.Main
Hello, World

按照規範,在類別路徑下被發現的類別,會自動歸類為未具名模組(Unnamed module),目前只要知道,基於相容性,未具名模組可以讀取其他模組;相對地,在模組路徑下被發現的類別,會是屬於某個具名模組(Named module),例如先前 --module-path 指定的 mods 中,cc.openhome 模組就是一種命名模組,名稱為 cc.openhome

編譯器與 module-path

假設現在基於某個原因,想將範例檔資料夾中 Hello1 的成果拆分為兩個模組,其中 cc.openhome.util 模組將會包含 cc.openhome.util.Console 類別,而 cc.openhome 模組將會包含 cc.openhome.Main,並依賴在 cc.openhome.util 模組的 cc.openhome.util.Console 類別,最後仍能夠執行顯示出「Hello, World」,該怎麼做呢?

基本上,可以如上建立起對應的資料夾與對應的 module-info.java,不過,這次必須在 module-info.java 做點設定,為了練習時的方便,可以直接複製範例檔資料夾中 Hello3 資料夾至 C:\workspace,其中已經建立好兩個模組應有的資料夾及 module-info.java 了,不過 module-info.java 還沒有任何額外的設定。

先來處理 cc.openhome.util 模組,為了讓其他模組能使用此模組下的 API,必須在 module-info.java 中使用 exports 宣告哪些套件是可公開的,因此請開啟 Hello3/src/cc.openhome.util/module-info.java 檔案,並如下進行設定:

module cc.openhome.util {
    exports cc.openhome.util;
}

現在模組中 cc.openhome.util 套件的 API,可以被其他模組使用,現在對 cc.openhome.util 模組進行編譯:

C:\workspace\Hello3>javac -d mods/cc.openhome.util src/cc.openhome.util/module-info.java

C:\workspace\Hello3>javac -d mods/cc.openhome.util src/cc.openhome.util/cc/openhome/util/Console.java

C:\workspace\Hello3>

因為 cc.openhome 模組依賴在 cc.openhome.util 模組,必須在 cc.openhome 模組的模組描述檔,使用 requires 宣告依賴的模組,請開啟 Hello3/src/cc.openhome/module-info.java,並如下進行設定:

module cc.openhome {
    requires cc.openhome.util;
}

接著對 cc.openhome 模組進行編譯,然後執行顯示 Hello, World:

C:\workspace\Hello3>javac --module-path mods -d mods/cc.openhome src/cc.openhome/module-info.java

C:\workspace\Hello3>javac --module-path mods -d mods/cc.openhome src/cc.openhome/cc/openhome/Main.java

C:\workspace\Hello3>java --module-path mods -m cc.openhome/cc.openhome.Main
Hello, World

在使用 javac 進行編譯時,也可以使用 --module-path 指定模組路徑,模組路徑下各模組的模組描述檔,包括了模組 API 的依賴、否存取關係,依此決定可否通過編譯。

編譯器與 module-source-path

在使用 javac 時,--module-source-path 可以指定模組的原始碼路徑,以方才的範例來說,若不想分別對 cc.openhome.utilcc.openhome 模組分別進行編譯,只要如下指定 --module-source-path 就可以了:

C:\workspace\Hello3>javac -d mods --module-source-path src src/cc.openhome/cc/openhome/Main.java

C:\workspace\Hello3>java --module-path mods -m cc.openhome/cc.openhome.Main
Hello, World

使用 --module-source-path 指定模組的原始碼路徑時,由於原始碼可能來自多個模組,因此搭配的 -d 引數在指定路徑時,只需要指定頂層資料夾,編譯器會自行建立起對應於模組名稱的資料夾。

在能夠運用 --module-path--module-source-path 引數之後,搭配 -d 引數時,本來就只需指定頂層資料夾,方才只是個循序漸進的過程,讓你逐步瞭解 --module-path--module-source-path 的作用,也就是說,如下編譯 cc.openhome.utilcc.openhome 模組就可以了:

C:\workspace\Hello3>javac -d mods --module-source-path src --module-path mods src/cc.openhome.util/cc/openhome/util/Console.java

C:\workspace\Hello3>javac -d mods --module-source-path src --module-path mods src/cc.openhome/cc/openhome/Main.java

C:\workspace\Hello3>

分享到 LinkedIn 分享到 Facebook 分享到 Twitter