文 by / 林本托
Tips
做一個終身學習的人。

在此章節中,主要介紹以下內容:
- 在JDK 9之前Java源代碼用於編寫,打包和部署的方式以及該方法的潛在問題
- JDK 9中有哪些模塊
- 如何聲明模塊及其依賴關系
- 如何封裝模塊
- 什么是模塊路徑
- 什么是可觀察的模塊
- 如何打印可觀察模塊的列表
- 如何打印模塊的描述
本章旨在為你簡要概述JDK 9中引入的模塊系統。后續章節將詳細介紹所有這些概念,並附有實例。 不要擔心,如果你第一次不了解所有模塊相關的概念。 一旦你獲得開發模塊代碼的經驗,你可以回來並重新閱讀本章。
一. Java 9 之前的開發
在 JDK 9之前,開發一個 Java 應用程序通常包括以下步驟:
- Java源代碼以Java類型(如類,接口,枚舉和注釋)的形式編寫。
- 不同的Java類型被安排在一個包(package)中,而且始終屬於一個明確或默認的包。 一個包是一個邏輯的類型集合,本質上為它包含的類型提供一個命名空間。 即使聲明為public,包可能包含公共類型,私有類型和一些內部實現類型。
- 編譯的代碼被打包成一個或多個JAR文件,也稱為應用程序JAR,因為它們包含應用程序代碼。 一個程序包中的代碼可能會引用多個JAR。
- 應用程序可能使用類庫。 類庫作為一個或多個JAR文件提供給應用程序使用。
- 通過將所有JAR文件,應用程序JAR文件和JAR類庫放在類路徑上來部署應用程序。
下圖顯示了JAR文件中打包的代碼的典型布局。 該圖僅顯示了包和Java 類型,不包括其他內容,如manifest.mf文件和資源文件。

20多年來,Java社區以這種編寫,編譯,打包和部署Java代碼的方式開發。 但是,20年漫長的旅程並沒有像你所希望的一樣順利! 這樣安排和運行Java代碼就存在固有的問題:
-
一個包只是一個類型的容器,而不強制執行任何可訪問性邊界。包中的公共類型可以在所有其他包中訪問;沒有辦法阻止在一個包中公開類型的全局可見性。
-
除了以java和javax開頭的包外,包應該是開放擴展的。如果你在具有包級別訪問的JAR中進行了類型化,則可以在其他JAR中訪問定義與你的名稱相同的包中的類型。
-
Java運行時會看到從JAR列表加載的一組包。沒有辦法知道是否在不同的JAR中有多個相同類型的副本。Java運行時首先加載在類路徑中遇到的JAR中找到的類型。
-
Java運行時可能會出現由於應用程序在類路徑中需要的其中一個JAR引起的運行時缺少類型的情況。當代碼嘗試使用它們時,缺少的類型會引起運行時錯誤。
-
在啟動時沒有辦法知道應用程序中使用的某些類型已經丟失。還可以包含錯誤的JAR文件版本,並在運行時產生錯誤。
這些問題在Java社區中非常頻繁和臭名昭着,他們得到了一個名字 ——JAR-hell。
包裝JDK和JRE也是一個問題。 它們作為一個整體作為使用,從而增加了下載時間,啟動時間和內存占用。 單體JRE使得Java不可能在內存很小的設備上使用。 如果將Java應用程序部署到雲端,則需要支付更多的費用購買更多的使用內存。 大多數情況下,單體JRE使用的內存比所需的內存多,這意味着需要為雲服務支付更多的內存。 Java 8中引入的Compact配置文件通過允許將JRE的一個子集打包在稱為緊湊配置文件的自定義運行時映像中,大大減少了JRE大小,從而減少了運行時內存占用。
Tips
在早期訪問版本中,JDK 9包含三個名為java.compact1,java.compact2和java.compact3的模塊,這些模塊對應於JDK 8中的三個compact配置文件。之后,它們被刪除,因為JDK中的模塊可以完全控制在自定義JRE中包含的模塊列表。
可以將JDK 9之前的JDK/JRE中的這些問題分為三類:
- 不可靠的配置
- 弱封裝
- JDK/JRE的單體結構
下圖顯示了Java運行時如何看到類路徑上的所有JAR,以及如何從其他JAR訪問一個JAR中的代碼,沒有任何限制,除了在訪問控制方面由類型聲明指定的代碼。

Java 9通過引入開發,打包和部署Java應用程序的新方法來解決這些問題。 在Java 9中,Java應用程序由稱為模塊的小型交互組件組成。 Java 9也已經將JDK/JRE組織為一組模塊。
二. 全新的模塊系統
Java 9引入了一個稱為模塊的新的程序組件。 您可以將Java應用程序視為具有明確定義的邊界和這些模塊之間依賴關系的交互模塊的集合。 模塊系統的開發具有以下目標:
- 可靠的配置
- 強封裝
- 模塊化JDK/JRE
這些目標是解決Java 9之前開發和部署Java應用程序所面臨的問題。
可靠的配置解決了用於查找類型的容易出錯的類路徑機制的問題。 模塊必須聲明對其他模塊的顯式依賴。 模塊系統驗證應用程序開發的所有階段的依賴關系 —— 編譯時,鏈接時和運行時。 假設一個模塊聲明對另一個模塊的依賴,並且第二個模塊在啟動時丟失。 JVM檢測到依賴關系丟失,並在啟動時失敗。 在Java 9之前,當使用缺少的類型時,這樣的應用程序會生成運行時錯誤(不是在啟動時)。
強大的封裝解決了類路徑上跨JAR的公共類型的可訪問性問題。 模塊必須明確聲明其中哪些公共類型可以被其他模塊訪問。 除非這些模塊明確地使其公共類型可訪問,否則模塊不能訪問另一個模塊中的公共類型。 Java 9中的公共類型並不意味着程序的所有部分都可以訪問它。 模塊系統增加了更精細的可訪問性控制。
Tips
Java 9通過允許模塊在開發的所有階段聲明明確的依賴關系並驗證這些依賴關系來提供可靠的配置。它通過允許模塊聲明其公共類型可以訪問其他模塊的軟件包來提供強大的封裝。
JDK 9通過將其前身的體結構分解成一組稱為平台模塊的模塊來重寫。 JDK 9還引入了一個可選的階段,稱為鏈接時,這可能在編譯時和運行時之間發生。 在鏈接期間,使用一個鏈接器,它是JDK 9附帶的一個名為jlink的工具,用於創建應用程序的自定義運行時映像,其中僅包含應用程序中使用的模塊。 這將運行時的大小調整到最佳大小。
三. 什么是模塊化
模塊是代碼和數據集合。 它可以包含Java代碼和本地代碼。 Java代碼被組織為一組包含諸如類,接口,枚舉和注解等類型的類。 數據可以包括諸如圖像文件和配置文件的資源。
對於Java代碼,模塊可以看做零個或多個包的集合。 下圖顯示了三個名為policy,claim和utility的模塊,其中policy模塊包含兩個包,claim模塊包含一個包,而utility模塊不包含任何包。

一個模塊不僅僅是一個包的容器。 除了其名稱,模塊定義包含以下內容:
- 所需的其他模塊(或依賴於)的列表
- 導出的軟件包列表(其公共API),其他模塊可以使用
- 開放的軟件包(其整個API,公共和私有)到其他反射訪問模塊的列表
- 使用的服務列表(或使用
java.util.ServiceLoader類發現和加載) - 提供的服務的實現列表
在使用這些模塊時,可以使用這些方面中的一個或多個。
Java SE 9平台規范將平台划分為稱為平台模塊的一組模塊。 Java SE 9平台的實現可能包含一些或所有平台模塊,從而提供可擴展的Java運行時。 標准模塊的名字是以Java 為前綴。 Java SE標准模塊的示例有java.base,java.sql,java.xml和java.logging。 支持標准平台模塊中的API,供開發人員使用。
非標准平台模塊是JDK的一部分,但未在Java SE平台規范中指定。 這些JDK特定的模塊的名稱以jdk為前綴。 JDK特定模塊的示例是jdk.charsets,jdk.compiler,jdk.jlink,jdk.policytool和jdk.zipfs。 JDK特定模塊中的API不適用於開發人員。 這些API通常用於JDK本身以及不能輕易獲得使用Java SE API所需功能的庫開發人員使用。 如果使用這些模塊中的API,則可能會在未經通知的情況下對其進行支持或更改。
JavaFX不是Java SE 9平台規范的一部分。 但是,在安裝JDK/JRE時,會安裝與JavaFX相關的模塊。 JavaFX模塊名稱以javafx為前綴。 JavaFX模塊的示例是javafx.base,javafx.controls,javafx.fxml,javafx.graphics和javafx.web。
作為Java SE 9平台的一部分的java.base模塊是原始模塊。 它不依賴於任何其他模塊。 模塊系統只知道java.base模塊。 它通過模塊中指定的依賴關系發現所有其他模塊。 java.base模塊導出核心Java SE軟件包,如java.lang,java.io,java.math,java.text,java.time,java.util等。
四. 模塊依賴關系
包括JDK 8之前的版本,一個包中的公共類型可以被其他包訪問,沒有任何限制。 換句話說,包沒有控制它們包含的類型的可訪問性。 JDK 9中的模塊系統對類型的可訪問性提供了細粒度的控制。
模塊之間的可訪問性是所使用的模塊和使用模塊之間的雙向協議:模塊明確地使其公共類型可供其他模塊使用,並且使用這些公共類型的模塊明確聲明對第一個模塊的依賴。 模塊中的所有未導出的軟件包都是模塊的私有的,它們不能在模塊之外使用。
將包中的 API 設置為公共供其他模塊使用被稱之為導出包。如果名為policy的模塊將名為pkg1的包設置為公共類型可用於其他模塊訪問,則說明policy模塊導出包pkg1。如果名為claim的模塊聲明對policy模塊的依賴性,則稱之為claim模塊讀取(read)policy模塊。這意味着,在claim模塊內部可以訪問policy模塊導出包中的所有公共類型。模塊還可以選擇性地將包導出到一個或多個命名模塊。這種導出成為qualified導出或module-friendly導出。 qualified導出中的包中的公共類型只能訪問指定的命名模塊。
在模塊系統的上下文中,可以互換使用三個術語 —— 需要(require),讀取(read)和依賴(depend)。 以下三個語句意思相同:P讀取Q,P需要Q,P依賴Q,其中P和Q指的是兩個模塊。
下圖描述了兩個名為policy和claim的模塊之間的依賴關系。 policy模塊包含兩個名為pkg1和pkg2的包,它導出包pkg1,該包使用虛線邊界顯示,以將其與未導出的包pkg2區分開來。 claim模塊包含兩個件包pkg3和pkg4,它不導出包。 它聲明了對policy模塊的依賴。

在JDK 9中,您可以如下聲明這兩個模塊:
module policy {
exports pkg1;
}
module claim {
requires policy;
}
Tips
用於指示模塊中的依賴關系的語法是不對稱的 ——導出一個包,但需要一個模塊。
如果你的模塊依賴於另一個模塊,則該模塊聲明要求知道模塊名稱。幾個Java框架和工具在很大程度上依賴於反射來在運行時訪問未導出的模塊的代碼。它們提供了很大的功能,如依賴注入,序列化,Java Persistence API的實現,代碼自動化和調試。Spring,Hibernate和XStream是這樣的框架和庫的例子。這些框架和庫不了解你的應用程序模塊。 但是,他們需要訪問模塊中的類型來完成他們的工作。 他們還需要訪問模塊的私有成員,這打破了JDK 9中強封裝的前提。當模塊導出軟件包時,依賴於第一個模塊的其他模塊只能訪問導出的軟件包中的公共API。 在運行時,在模塊的所有軟件包上授予深入的反射訪問權限(訪問公共和私有API),可以聲明一個開放的模塊。

在JDK 9中,可以如下聲明這兩個模塊:
open module policy.model {
requires jdojo.jpa;
}
module jdojo.jpa {
// The module exports its packages here
}
1. 模塊圖
模塊系統只知道一個模塊:java.base。 java.base模塊不依賴於任何其他模塊。 所有其他模塊都隱含地依賴於java.base模塊。
應用程序的模塊化結構可以被視為一個稱為模塊圖。 在模塊圖中,每個模塊都表示為一個節點。 如果第一個模塊依賴於第二個模塊,則存在從模塊到另一個模塊的有向邊。 通過將稱為根模塊的一組初始模塊的依賴關系與稱為可觀察模塊的模塊系統已知的一組模塊相結合來構建模塊圖。
Tips
模塊解析意味着該模塊所依賴的模塊可用。 假設名為P的模塊取決於兩個名為Q和R的模塊。解析模塊P表示您定位模塊Q和R,並遞歸地解析模塊Q和R。
構建模塊圖旨在在編譯時,鏈接時和運行時解析模塊依賴關系。 模塊解析從根模塊開始,並遵循依賴關系鏈接,直到達到java.base模塊。 有時,可能在模塊路徑上有一個模塊,但是會收到該模塊未找到的錯誤。 如果模塊未解析,並且未包含在模塊圖中,則可能會發生這種情況。 對於要解決的模塊,需要從根模塊開始依賴關系鏈。 根據調用編譯器或Java啟動器的方式,選擇一組默認的根模塊。 還可以將模塊添加到默認的根模塊中。 了解在不同情況下如何選擇默認根模塊很重要:
- 如果應用程序代碼是從類路徑編譯的,或者主類是從類路徑運行的,則默認的根模塊將由java.se模塊和所有非“java.”系統模塊組成,如“jdk.”和“JavaFX.”。 如果java.se模塊不存在,則默認的根模塊將由所有“java.”和非“java.*”模塊組成。
- 如果您的應用程序由模塊組成,則默認的根模塊將依賴於以下階段:
- 在編譯時,它由所有正在編譯的模塊組成。
- 在鏈接時,它是空的。
- 在運行時,它包含有主類的模塊。 在java命令中使用
--module或-m選項指定要運行的模塊及其主類。
繼續介紹policy和claim模塊的例子,假設pkg3.Main是claim模塊中的主類,並且兩個模塊都作為模塊化JAR打包在C:\ Java9Revealed\lib目錄中。下圖顯示了使用以下命令運行應用程序時在運行時構建的模塊圖:

C:\Java9Revealed>java -p lib -m claim/pkg3.Main
claim模塊包含應用程序的主類。 因此,claim是創建模塊圖時唯一的根模塊。 policy模塊需要被解決,因為claim模塊依賴於policy模塊。 還需要解析java.base模塊,因為所有其他模塊都依賴於它,這兩個模塊也是如此。
模塊圖的復雜性取決於根模塊的數量和模塊之間的依賴關系。 假設除了依賴於policy模塊之外,claim模塊還取決於java.sql的平台模塊。 claim模塊的新聲明如下所示:
module policy {
requires policy;
requires java.sql;
}
如下圖,顯示在claim模塊中運行pkg3.Main類時將構建的模塊圖。 請注意,java.xml和java.logging模塊也存在於圖中,因為java.sql模塊依賴於它們。 在圖中,claim模塊是唯一的根模塊。

下圖顯示了java.se的平台模塊的最復雜的模塊圖形之一。 java.se模塊的模塊聲明如下:

java.se 模塊的定義如下所示:
module java.se {
requires transitive java.sql;
requires transitive java.rmi;
requires transitive java.desktop;
requires transitive java.security.jgss;
requires transitive java.security.sasl;
requires transitive java.management;
requires transitive java.logging;
requires transitive java.xml;
requires transitive java.scripting;
requires transitive java.compiler;
requires transitive java.naming;
requires transitive java.instrument;
requires transitive java.xml.crypto;
requires transitive java.prefs;
requires transitive java.sql.rowset;
requires java.base;
requires transitive java.datatransfer;
}
有時,需要將模塊添加到默認的根模塊中,以便解析添加的模塊。 可以在編譯時,鏈接時和運行使用--add-modules命令行選項指定其他根模塊:
--add-modules <module-list>
這里的<module-list>是逗號分隔的模塊名稱列表。
可以使用以下特殊值作為具有特殊含義的--add-modules選項的模塊列表:
- ALL-DEFAULT
- ALL-SYSTEM
- ALL-MODULE-PATH
所有三個特殊值在運行時都有效。 只能在編譯時使用ALL-MODULE-PATH。
如果使用ALL-DEFAULT作為模塊列表,則從應用程序從類路徑運行時使用的默認的根模塊集將添加到根集中。 這對於作為容器的應用程序是有用的,托管可能需要容器應用程序本身不需要的其他模塊的其他應用程序。 這是一種使所有Java SE模塊可用於容器的方法,因此任何托管的應用程序都可能使用到它們。
如果將ALL-SYSTEM用作模塊列表,則將所有系統模塊添加到根集中。 這對於運行測試時非常有用。
如果使用ALL-MODULE-PATH作為模塊列表,則在模塊路徑上找到的所有模塊都將添加到根集中。 這對於諸如Maven這樣的工具非常有用,這確保了應用程序需要模塊路徑上的所有模塊。
Tips
即使模塊存在於模塊路徑上,也可能會收到模塊未找到的錯誤。 在這種情況下,需要使用--add-modules命令行選項將缺少的模塊添加到默認的根模塊中。
JDK 9支持一個有用的非標准命令行選項,它打印描述在構建模塊圖時用於解析模塊的步驟的診斷消息。 選項是 -Xdiag:resolver。 以下命令在聲明模塊中運行pkg3.Main類。 顯示部分輸出。 在診斷消息的結尾,你會發現一個結果:部分列出了解決模塊。
使用命令C:\Java9Revealed>java -Xdiag:resolver -p lib -m claim/pkg3.Main,會得到如下輸出:
[Resolver] Root module claim located
[Resolver] (file:///C:/Java9Revealed/lib/claim.jar)
[Resolver] Module java.base located, required by claim
[Resolver] (jrt:/java.base)
[Resolver] Module policy located, required by claim
[Resolver] (file:///C:/Java9Revealed/lib/policy.jar)
...
[Resolver] Result:
[Resolver] claim
[Resolver] java.base
...
[Resolver] policy
五. 聚合模塊
你可以創建一個不包含任何代碼的模塊。 它收集並重新導出其他模塊的內容。 這樣的模塊稱為聚合模塊。假設有幾個模塊依賴於五個模塊。 您可以為這五個模塊創建一個聚合模塊,現在,你的模塊只能依賴於一個模塊 —— 聚合模塊。
為了方便, Java 9包含幾個聚合模塊,如java.se和java.se.ee。 java.se模塊收集Java SE的不與Java EE重疊的部分。 java.se.ee模塊收集組成Java SE的所有模塊,包括與Java EE重疊的模塊。
六. 聲明模塊
本節包含用於聲明模塊的語法的快速概述。 在以后的章節中更詳細地解釋每個部分。 如果不明白本節提到的模塊,請繼續閱讀。
使用模塊聲明來定義模塊,是Java編程語言中的新概念。其語法如下:
[open] module <module> {
<module-statement>;
<module-statement>;
...
}
open修飾符是可選的,它聲明一個開放的模塊。 一個開放的模塊導出所有的包,以便其他模塊使用反射訪問。 <module>是要定義的模塊的名稱。 <module-statement>是一個模塊語句。 模塊聲明中可以包含零個或多個模塊語句。 如果它存在,它可以是五種類型的語句之一:
- 導出語句(exports statement);
- 開放語句(opens statement);
- 需要語句(requires statement);
- 使用語句(uses statement);
- 提供語句(provides statement)。
導出和開放語句用於控制對模塊代碼的訪問。 需要語句用於聲明模塊對另一個模塊的依賴關系。 使用和提供的語句分別用於表達服務消費和服務提供。 以下是名為myModule的模塊的模塊聲明示例:
module myModule {
// Exports the packages - com.jdojo.util and
// com.jdojo.util.parser
exports com.jdojo.util;
exports com.jdojo.util.parser;
// Reads the java.sql module
requires java.sql;
// Opens com.jdojo.legacy package for reflective access
opens com.jdojo.legacy;
// Uses the service interface java.sql.Driver
uses java.sql.Driver;
// Provides the com.jdojo.util.parser.FasterCsvParser
// class as an implementation for the service interface
// named com.jdojo.util.CsvParser
provides com.jdojo.util.CsvParser
with com.jdojo.util.parser.FasterCsvParser;
}
你可以使用模塊聲明中的open修飾符來創建一個開放模塊。 一個開放模塊可以將其所有軟件包的反射訪問授予其他模塊。 你不能在open模塊中再使用open語句,因為所有程序包都是在open模塊中隱式打開的。 以下代碼段聲明一個名為myLegacyModule的開放模塊:
open module myLegacyModule {
exports com.jdojo.legacy;
requires java.sql;
}
1. 模塊命名
模塊名稱可以是Java限定標識符。 合法標識符是一個或多個由點分隔的標識符,例如policy,com.jdojo.common和com.jdojo.util。 如果模塊名稱中的任何部分不是有效的Java標識符,則會發生編譯時錯誤。 例如,com.jdojo.common.1.0不是有效的模塊名稱,因為名稱中的1和0不是有效的Java標識符。
與包命名約定類似,使用反向域名模式為模塊提供唯一的名稱。 使用這個慣例,名為com.jdojo.common的最簡單的模塊可以聲明如下:
module com.jdojo.common {
// No module statements
}
模塊名稱不會隱藏具有相同名稱的變量,類型和包。 因此,可以擁有一個模塊以及具有相同名稱的變量,類型或包。 他們使用的上下文將區分哪個名稱是指什么樣的實體。
在JDK 9中, open, module, requires, transitive, exports, opens, to, uses, provides 和 with是受限關鍵字。只有當具體位置出現在模塊聲明中時,它們才具有特殊意義。 可以將它們用作程序中其他地方的標識符。 例如,以下模塊聲明是有效的,即使它不使用直觀的模塊名稱:
// Declare a module named module
module module {
// Module statements go here
}
第一個模塊字被解釋為一個關鍵字,第二個是一個模塊名稱。
你可以在程序中的任何地方聲明一個名為module的變量:
String module = "myModule";
2. 模塊的訪問控制
導出語句將模塊的指定包導出到所有模塊或編譯時和運行時的命名模塊列表。 它的兩種形式如下:
exports <package>;
exports <package> to <module1>, <module2>...;
以下是使用了導出語句的模塊示例:
module M {
exports com.jdojo.util;
exports com.jdojo.policy
to com.jdojo.claim, com.jdojo.billing;
}
開放語句允許對所有模塊的反射訪問指定的包或運行時指定的模塊列表。 其他模塊可以使用反射訪問指定包中的所有類型以及這些類型的所有成員(私有和公共)。 開放語句采用以下形式:
opens <package>;
opens <package> to <module1>, <module2>...;
使用開放語句的實例:
module M {
opens com.jdojo.claim.model;
opens com.jdojo.policy.model to core.hibernate;
opens com.jdojo.services to core.spring;
}
Tips
對比導出和打開語句。 導出語句允許僅在編譯時和運行時訪問指定包的公共API,而打開語句允許在運行時使用反射訪問指定包中的所有類型的公共和私有成員。
如果模塊需要在編譯時從另一個模塊訪問公共類型,並在運行時使用反射訪問類型的私有成員,則第二個模塊可以導出並打開相同的軟件包,如下所示:
module N {
exports com.jdojo.claim.model;
opens com.jdojo.claim.model;
}
閱讀有關模塊的時候會遇到三個短語:
- 模塊M導出包P
- 模塊M打開包Q
- 模塊M包含包R
前兩個短語對應於模塊中導出語句和開放語句。 第三個短語意味着該模塊包含的包R既不導出也不開放。 在模塊系統的早期設計中,第三種情況被稱為“模塊M隱藏包R”。
3. 聲明依賴關系
需要(require)語句聲明當前模塊與另一個模塊的依賴關系。 一個名為M的模塊中的“需要N”語句表示模塊M取決於(或讀取)模塊N。語句有以下形式:
requires <module>;
requires transitive <module>;
requires static <module>;
requires transitive static <module>;
require語句中的靜態修飾符表示在編譯時的依賴是強制的,但在運行時是可選的。requires static N語句意味着模塊M取決於模塊N,模塊N必須在編譯時出現才能編譯模塊M,而在運行時存在模塊N是可選的。require語句中的transitive修飾符會導致依賴於當前模塊的其他模塊具有隱式依賴性。假設有三個模塊P,Q和R,假設模塊Q包含requires transitive R語句,如果如果模塊P包含包含requires Q語句,這意味着模塊P隱含地取決於模塊R。
4. 配置服務
Java允許使用服務提供者和服務使用者分離的服務提供者機制。 JDK 9允許使用語句(uses statement)和提供語句(provides statement)實現其服務。
使用語句可以指定服務接口的名字,當前模塊就會發現它,使用 java.util.ServiceLoader類進行加載。格式如下:
uses <service-interface>;
使用語句的實例如下:
module M {
uses com.jdojo.prime.PrimeChecker;
}
com.jdojo.PrimeChecker是一個服務接口,其實現類將由其他模塊提供。 模塊M將使用java.util.ServiceLoader類來發現和加載此接口的實現。
提供語句指定服務接口的一個或多個服務提供程序實現類。 它采取以下形式:
provides <service-interface>
with <service-impl-class1>, <service-impl-class2>...;
相同的模塊可以提供服務實現,可以發現和加載服務。 模塊還可以發現和加載一種服務,並為另一種服務提供實現。 以下是例子:
module P {
uses com.jdojo.CsvParser;
provides com.jdojo.CsvParser
with com.jdojo.CsvParserImpl;
provides com.jdojo.prime.PrimeChecker
with com.jdojo.prime.generic.FasterPrimeChecker;
}
七. 模塊描述符
在了解上一節中如何聲明模塊之后,你可能會對模塊聲明的源代碼有幾個疑問:
- 在哪里保存模塊聲明的源代碼? 是否保存在文件中? 如果是,文件名是什么?
- 在哪里放置模塊聲明源代碼文件?
- 模塊的聲明的源代碼如何編譯?
1. 編譯模塊聲明
模塊聲明存儲在名為module-info.java的文件中,該文件存儲在該模塊的源文件層次結構的根目錄下。 Java編譯器將模塊聲明編譯為名為module-info.class的文件。 module-info.class文件被稱為模塊描述符,它被放置在模塊的編譯代碼層次結構的根目錄下。 如果將模塊的編譯代碼打包到JAR文件中,則module-info.class文件將存儲在JAR文件的根目錄下。
模塊聲明不包含可執行代碼。 實質上,它包含一個模塊的配置。 那為什么我們不在XML或JSON格式的文本文件中保留模塊聲明,而是在類文件中? 類文件被選為模塊描述符,因為類文件具有可擴展,明確定義的格式。 模塊描述符包含源碼級模塊聲明的編譯形式。 它可以通過工具來增強,例如 jar工具,在模塊聲明初始編譯之后,在類文件屬性中包含附加信息。 類文件格式還允許開發人員在模塊聲明中使用導入和注解。
2. 模塊版本
在模塊系統的初始原型中,模塊聲明還包括模塊版本的。 包括模塊版本在聲明中使模塊系統的實現復雜化,所以模塊版本從聲明中刪除。
模塊描述符(類文件格式)的可擴展格式被利用來向模塊添加版本。 當將模塊的編譯代碼打包到JAR中時,該jar工具提供了一個添加模塊版本的選項,最后將其添加到module-info.class文件中。
3. 模塊源文件結構
我們來看一個組織源代碼和一個名為com.jdojo.contact的模塊的編譯代碼的例子。 該模塊包含用於處理聯系信息的包,例如地址和電話號碼。 它包含兩個包:
- com.jdojo.contact.info
- com.jdojo.contact.validator
com.jdojo.contact.info包中包含兩個類 —— Address 和 Phone。 com.jdojo.contact.validator包中包含一個名為Validator的接口和兩個名為AddressValidator和PhoneValidator的類。
下圖顯示了com.jdojo.contact模塊中的內容

在Java 9中,Java編譯器工具javac添加了幾個選項。 它允許一次編譯一個模塊或多個模塊。 如果要一次編譯多個模塊,則必須將每個模塊的源代碼存儲在與模塊名稱相同的目錄下。 即使只有一個模塊,也最好遵循此源目錄命名約定。
假設你想編譯com.jdojo.contact模塊的源代碼。 可以將其源代碼存儲在名為C:\j9r\src的目錄中,其中包含以下文件:
module-info.java
com\jdojo\contact\info\Address.java
com\jdojo\contact\info\Phone.java
com\jdojo\contact\validator\Validator.java
com\jdojo\contact\validator\AddressValidator.java
com\jdojo\contact\validator\PhoneValidator.java
請注意,需要遵循包層次結構來存儲接口和類的源文件。
如果要一次編譯多個模塊,則必須將源代碼目錄命名為com.jdojo.contact,這與模塊的名稱相同。 在這種情況下,可以將模塊的源代碼存儲在名為C:\j9r\src的目錄中,其目錄如下:
com.jdojo.contact\module-info.java
com.jdojo.contact\com\jdojo\contact\info\Address.java
com.jdojo.contact\com\jdojo\contact\info\Phone.java
com.jdojo.contact\com\jdojo\contact\validator\Validator.java
com.jdojo.contact\com\jdojo\contact\validator\AddressValidator.java
com.jdojo.contact\com\jdojo\contact\validator\PhoneValidator.java
模塊的編譯后代碼將遵循與之前看到的相同的目錄層次結構。
八. 打包模塊
模塊的artifact可以存儲在:
- 目錄中
- 模塊化的JAR文件中
- JMOD文件中,它是JDK 9中引入的一種新的模塊封裝格式
1. 目錄中的模塊
當模塊的編譯代碼存儲在目錄中時,目錄的根目錄包含模塊描述符(module-info.class文件),子目錄是包層次結構的鏡像。 繼續上一節中的示例,假設將com.jdojo.contact模塊的編譯代碼存儲在C:\j9r\mods\ com.jdojo.contact目錄中。 目錄的內容如下:
com\jdojo\contact\info\Address.class
com\jdojo\contact\info\Phone.class
com\jdojo\contact\validator\Validator.class
com\jdojo\contact\validator\AddressValidator.class
com\jdojo\contact\validator\PhoneValidator.class
2. 模塊化JAR中的模塊
JDK附帶一個jar工具,以JAR(Java Archive)文件格式打包Java代碼。 JAR格式基於ZIP文件格式。 JDK 9增強了在JAR中打包模塊代碼的jar工具。 當JAR包含模塊的編譯代碼時,JAR稱為模塊化JAR。 模塊化JAR在根目錄下包含一個module-info.class文件。
無論在JDK 9之前使用JAR,現在都可以使用模塊化JAR。 例如,模塊化JAR可以放置在類路徑上,在這種情況下,模塊化JAR中的module-info.class文件將被忽略,因為module-info在Java中不是有效的類名。
在打包模塊化JAR的同時,可以使用JDK 9中添加的jar工具中可用的各種選項,將模塊描述符中的信息例如模塊版本添加到主類中。
Tips
模塊化JAR在各個方面來看都是一個JAR,除了它在根路徑下包含的模塊描述符。通常,比較重要的Java應用程序由多個模塊組成。 模塊化JAR可以是一個模塊,包含編譯的代碼。 需要將應用程序的所有模塊打包到單個JAR中。
繼續上一節中的示例,com.jdojo.contact模塊的模塊化JAR內容如下。 請注意,JAR在META-INF目錄中始終包含一個MANIFEST.MF文件。
module-info.class
com/jdojo/contact/info/Address.class
com/jdojo/contact/info/Phone.class
com/jdojo/contact/validator/Validator.class
com/jdojo/contact/validator/AddressValidator.class
com/jdojo/contact/validator/PhoneValidator.class
META-INF/MANIFEST.MF
3. JMOD文件中的模塊
JDK 9引入了一種稱為JMOD的新格式來封裝模塊。 JMOD文件使用.jmod擴展名。 JDK模塊被編譯成JMOD格式,放在JDK_HOME \ jmods目錄中。例如,可以找到一個包含java.base模塊內容的java.base.jmod文件。 僅在編譯時和鏈接時才支持JMOD文件。 它們在運行時不受支持。
九. 模塊路徑
自JDK開始以來,類路徑機制查找類型已經存在。 類路徑是一系列目錄,JAR文件和ZIP文件。 當Java需要在各個階段(編譯時,運行時,工具使用等)中查找類型時,它會使用類路徑中的條目來查找類型。
Java 9類型作為模塊的一部分存在。 Java需要在不同階段查找模塊,而不是類似於Java 9之前的模塊。Java 9引入了一種新的機制來查找模塊,它被稱為模塊路徑。
模塊路徑是包含模塊的路徑名稱序列,其中路徑名可以是模塊化JAR,JMOD文件或目錄的路徑。 路徑名由特定於平台的路徑分隔符分隔,在UNIX平台上為冒號(😃,Windows平台上分號(;)。
當路徑名稱是模塊化的JAR或JMOD文件時,很容易理解。 在這種情況下,如果JAR或JMOD文件中的模塊描述符包含要查找的模塊的模塊定義,則會找到該模塊。 如果路徑名是目錄,則存在以下兩種情況:
- 如果類文件存在於根目錄,則該目錄被認為具有模塊定義。 根目錄下的類文件將被解釋為模塊描述符。 所有其他文件和子目錄將被解釋為此一個模塊的一部分。 如果根目錄中存在多個類文件,則首先找到的文件被解釋為模塊描述符。 經過幾次實驗,JDK 9似乎以按字母排列的順序拾取了第一類文件。 這種存儲模塊編譯代碼的方式肯定會讓你頭疼。 因此,如果目錄在根目錄中包含多個類文件,請避免向模塊路徑添加目錄。
- 如果根目錄中不存在類文件,則目錄的內容將被不同的解釋。 目錄中的每個模塊化JAR或JMOD文件被認為是模塊定義。 每個子目錄,如果它包含在它的根一個 module-info.class文件,被認為具有展開目錄樹格式的模塊定義。 如果一個子目錄的根目錄不包含一個module-info.class文件,那么它不會被解釋為包含一個模塊定義。 請注意,如果子目錄包含模塊定義,則其名稱不必與模塊名稱相同。 模塊名稱是從module-info.class文件中讀取的。
以下是Windows上的有效模塊路徑:
- C:\mods
- C:\mods\com.jdojo.contact.jar;C:\mods\com.jdojo.person.jar
- C:\lib;C:\mods\com.jdojo.contact.jar;C:\mods\com.jdojo.person.jar
第一個模塊路徑包含名為C:\mods的目錄的路徑。 第二個模塊路徑包含兩個模塊化JAR——com.jdojo.contact.jar和com.jdojo.person.jar的路徑。 第三個模塊路徑包含三個元素 —— 目錄C:\lib的路徑,以及兩個模塊化JAR——com.jdojo.contact.jar和com.jdojo.person.jar的路徑。 在類似UNIX的平台上顯示相當於這些路徑:
- /usr/ksharan/mods
- /usr/ksharan/mods/com.jdojo.contact.jar:/usr/ksharan/com.jdojo.person.jar
- /usr/ksharan/lib:/usr/ksharan/mods/com.jdojo.contact.jar:/usr/ksharan/mods/com.jdojo.person.jar
避免模塊路徑問題的最佳方法是不要將分解的目錄用作模塊定義。
有兩個目錄作為模塊路徑 —— 一個包含所有應用程序模塊化JAR的目錄,另一個包含用於外部庫的所有模塊化JAR的目錄。例如,可以使用C:\applib 和 C:\extlib作為Windows上的模塊路徑,其中C:\applib目錄包含所有應用程序模塊化JAR,C:\extlib目錄包含所有外部庫的模塊化JAR。
JDK 9已經更新了所有的工具來使用模塊路徑來查找模塊。這些工具提供了指定模塊路徑的新選項。到JDK 9,已經看到以一個連字符(-)開頭的UNIX樣式選項,例如-cp和-classpath。在JDK 9中有如此多的附加選項,JDK設計人員對於開發人員來說也用完了有意義的短名稱的選項。因此,JDK 9開始使用GNU樣式選項,其中選項以兩個連續的連字符開頭,並且單詞由連字符分隔。以下是GNU樣式命令行選項的幾個示例:
- --class-path
- --module-path
- --module-version
- --main-class
- --print-module-descriptor
Tips
要打印工具支持的所有標准選項的列表,使用--help或-h選項運行該工具,對於所有非標准選項,使用-X選項運行該工具。 例如,java -h和java -X命令將分別打印java命令的標准和非標准選項列表。
JDK 9中的大多數工具(如javac,java和jar)都支持兩個選項來在命令行上指定一個模塊路徑。 它們是-p和--module-path。 將繼續支持現有的UNIX樣式選項以實現向后兼容性。 以下兩個命令顯示如何使用兩個選項來指定java工具的模塊路徑:
// Using the GNU-style option
C:\>java --module-path C:\applib;C:\lib other-args-go-here
// Using the UNIX-style option
C:\>java -p C:\applib;C:\extlib other-args-go-here
當您使用GNU樣式選項時,可以使用以下兩種形式之一指定該選項的值:
- --
- --
=
上面的命令也可以寫成如下形式:
// Using the GNU-style option
C:\>java --module-path=C:\applib;C:\lib other-args-go-here
當使用空格作為名稱值分隔符時,需要至少使用一個空格。 您使用=作為分隔符時,不得在其周圍包含任何空格。
十. 可觀察模塊
在模塊查找過程中,模塊系統使用不同類型的模塊路徑來定位模塊。 在模塊路徑上與系統模塊一起發現的一組模塊被稱為可觀察模塊。 可以將可觀察模塊視為模塊系統在特定階段可用的所有模塊的集合,例如編譯時,鏈接時和運行時,或可用於工具。
JDK 9為java命令添加了一個名為--list-modules的新選項。 該選項可用於打印兩種類型的信息:可觀察模塊的列表和一個或多個模塊的描述。 該選項可以以兩種形式使用:
- --list-modules
- --list-modules
, ...
在第一種形式中,該選項沒有跟隨任何模塊名稱。 它打印可觀察模塊的列表。 在第二種形式中,該選項后面是逗號分隔的模塊名稱列表,用於打印指定模塊的模塊描述符。
以下命令打印可觀察模塊的列表,其中僅包括系統模塊:
c:\Java9Revealed> java --list-modules
java.base@9-ea
java.se.ee@9-ea
java.sql@9-ea
javafx.base@9-ea
javafx.controls@9-ea
jdk.jshell@9-ea
jdk.unsupported@9-ea
...
上面顯示的是輸出部分內容。 輸出中的每個條目都包含兩個部分—— 一個模塊名稱和一個由@符號分隔的版本字符串。 第一部分是模塊名稱,第二部分是模塊的版本字符串。 例如,在java.base@9-ea中,java.base是模塊名稱,9-ea是版本字符串。 在版本字符串中,數字9表示JDK 9,ea代表早期訪問。 運行命令時,你可能會得到不同的版本字符串輸出。
現在在C:\Java9Revealed\lib目錄中放置了三個模塊化JAR。 如果提供此目錄作為java命令的模塊路徑,這些模塊將被包含在可觀察模塊列表中。以下命令顯示了改變指定一個模塊路徑后,觀察到的模塊列表。 這里,lib目錄是相對路徑,C:\Java9Revealed是當前目錄。
C:\Java9Revealed>java --module-path lib --list-modules
claim (file:///C:/Java9Revealed/lib/claim.jar)
policy (file:///C:/Java9Revealed/lib/policy.jar)
java.base@9-ea
java.xml@9-ea
javafx.base@9-ea
jdk.unsupported@9-ea
jdk.zipfs@9-ea
...
注意,對於應用程序模塊,--list-modules選項還會打印它們的位置。 當獲得意想不到的結果,並且不知道正在使用哪些模塊以及哪些位置時,此信息有助於排除故障。
以下命令將com.jdojo.intro模塊指定為--list-modules選項的參數,以打印模塊的描述:
C:\Java9Revealed>java --module-path lib --list-modules claim
module claim (file:///C:/Java9Revealed/lib/claim.jar)
exports com.jdojo.claim
requires java.sql (@9-ea)
requires mandated java.base (@9-ea)
contains pkg3
輸出的第一行包含模塊名稱和包含該模塊的模塊化JAR位置。 第二行表示該模塊導出com.jdojo.claim模塊。 第三行表示該模塊需要java.sql模塊。 第四行表示模塊強制依賴於java.base模塊。 回想一下,除了java.base模塊之外的每個模塊都取決於java.base模塊。 除了java.base模塊,在每個模塊的描述中看到需要強制的java.base模塊。 第五行聲明該模塊包含一個名為pkg3的包,既不導出也不開放。
你還可以使用--list-modules打印系統模塊的描述,例如java.base和java.sql。 以下命令打印出java.sql模塊的描述。
C:\Java9Revealed>java --list-modules java.sql
module java.sql@9-ea
exports java.sql
exports javax.sql
exports javax.transaction.xa
requires transitive java.xml
requires mandated java.base
requires transitive java.logging
uses java.sql.Driver
十一. 總結
Java中的包已被用作類型的容器。 應用程序由放置在類路徑上的幾個JAR組成。 軟件包作為類型的容器,不強制執行任何可訪問性邊界。 類型的可訪問性內置在使用修飾符的類型聲明中。 如果包中包含內部實現,則無法阻止程序的其他部分訪問內部實現。 類路徑機制在使用類型時線性搜索類型。 這導致在部署的JAR中缺少類型時,在運行時接收錯誤的另一個問題 —— 有時在部署應用程序后很長時間。 這些問題可以分為兩種類型:封裝和配置。
JDK 9引入了模塊系統。 它提供了一種組織Java程序的方法。 它有兩個主要目標:強大的封裝和可靠的配置。 使用模塊系統,應用程序由模塊組成,這些模塊被命名為代碼和數據的集合。 模塊通過其聲明來控制模塊的其他模塊可以訪問的部分。 訪問另一個模塊的部分的模塊必須聲明對第二個模塊的依賴。 控制訪問和聲明依賴的是達成強封裝的基礎。 在應用程序啟動時解決了一個模塊的依賴關系。 在JDK 9中,如果一個模塊依賴於另一個模塊,並且運行應用程序時第二個模塊丟失,則在啟動時將會收到一個錯誤,而不是應用程序運行后的某個時間。 這是一個可靠的基礎配置。
使用模塊聲明定義模塊。 模塊的源代碼通常存儲在名為module-info.java的文件中。 一個模塊被編譯成一個類文件,通常命名為module-info.class。 編譯后的模塊聲明稱為模塊描述符。 模塊聲明不允許指定模塊版本。 但諸如將模塊打包到JAR中的jar工具的可以將模塊版本添加到模塊描述符中。
使用module關鍵字聲明模塊,后跟模塊名稱。 模塊聲明可以使用五種類型的模塊語句:exports,opens,require,uses和provide。 導出語句將模塊的指定包導出到所有模塊或編譯時和運行時的命名模塊列表。 開放語句允許對所有模塊的反射訪問指定的包或運行時指定的模塊列表, 其他模塊可以使用反射訪問指定包中的所有類型以及這些類型的所有成員(私有和公共)。 使用語句和提供模塊語句用於配置模塊以發現服務實現並提供特定服務接口的服務實現。
從JDK 9開始,open, module, requires, transitive, exports,opens,to,uses,provides和with都是受限關鍵字。 只有當具體位置出現在模塊聲明中時,它們才具有特殊意義。
模塊的源代碼和編譯代碼被安排在目錄,JAR文件或JMOD文件中。 在目錄和JAR文件中,module-info.class文件位於根目錄。
與類路徑類似,JDK 9引入了模塊路徑。 但是,它們的使用方式有所不同。 類路徑用於搜索類型的定義,而模塊路徑用於查找模塊,而不是模塊中的特定類型。 Java工具(如java和javac)已經被更新為使用模塊路徑和類路徑。 您可以使用--module-path或-p選項指定這些工具的模塊路徑。
JDK 9引入了與工具一起使用的GNU風格選項。 選項以兩個破折號開頭,每個單詞用短划線分隔,例如--module-path,--class-path,--list-modules等。如果選項接受一個值,則該值可以跟隨選項加上空格或=。 以下兩個選項是一樣的:
- --module-path C:\lib
- --module-path=C:\lib
模塊系統在某個階段(編譯時,運行時,工具等)中可用的模塊列表被稱為可觀察模塊。 可以使用--list-modules選項與java命令列出運行時可用的可觀察模塊。 還可以使用此選項打印模塊的描述。
