初探模組
May 19, 2022Java 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.util
與 cc.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.util
及 cc.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>