Java 9 揭祕(2. 模塊化系統)

文 by / 林本託java

Tips
作一個終身學習的人。web

Java 9

在此章節中,主要介紹如下內容:spring

  • 在JDK 9以前Java源代碼用於編寫,打包和部署的方式以及該方法的潛在問題
  • JDK 9中有哪些模塊
  • 如何聲明模塊及其依賴關係
  • 如何封裝模塊
  • 什麼是模塊路徑
  • 什麼是可觀察的模塊
  • 如何打印可觀察模塊的列表
  • 如何打印模塊的描述

本章旨在爲你簡要概述JDK 9中引入的模塊系統。後續章節將詳細介紹全部這些概念,並附有實例。 不要擔憂,若是你第一次不瞭解全部模塊相關的概念。 一旦你得到開發模塊代碼的經驗,你能夠回來並從新閱讀本章。sql

一. Java 9 以前的開發

在 JDK 9以前,開發一個 Java 應用程序一般包括如下步驟:shell

  • Java源代碼以Java類型(如類,接口,枚舉和註釋)的形式編寫。
  • 不一樣的Java類型被安排在一個包(package)中,並且始終屬於一個明確或默認的包。 一個包是一個邏輯的類型集合,本質上爲它包含的類型提供一個命名空間。 即便聲明爲public,包可能包含公共類型,私有類型和一些內部實現類型。
  • 編譯的代碼被打包成一個或多個JAR文件,也稱爲應用程序JAR,由於它們包含應用程序代碼。 一個程序包中的代碼可能會引用多個JAR。
  • 應用程序可能使用類庫。 類庫做爲一個或多個JAR文件提供給應用程序使用。
  • 經過將全部JAR文件,應用程序JAR文件和JAR類庫放在類路徑上來部署應用程序。

下圖顯示了JAR文件中打包的代碼的典型佈局。 該圖僅顯示了包和Java 類型,不包括其餘內容,如manifest.mf文件和資源文件。編程

JAR 內部佈局

20多年來,Java社區以這種編寫,編譯,打包和部署Java代碼的方式開發。 可是,20年漫長的旅程並無像你所但願的同樣順利! 這樣安排和運行Java代碼就存在固有的問題:app

  • 一個包只是一個類型的容器,而不強制執行任何可訪問性邊界。包中的公共類型能夠在全部其餘包中訪問;沒有辦法阻止在一個包中公開類型的全局可見性。框架

  • 除了以java和javax開頭的包外,包應該是開放擴展的。若是你在具備包級別訪問的JAR中進行了類型化,則能夠在其餘JAR中訪問定義與你的名稱相同的包中的類型。
  • Java運行時會看到從JAR列表加載的一組包。沒有辦法知道是否在不一樣的JAR中有多個相同類型的副本。Java運行時首先加載在類路徑中遇到的JAR中找到的類型。編程語言

  • Java運行時可能會出現因爲應用程序在類路徑中須要的其中一個JAR引發的運行時缺乏類型的狀況。當代碼嘗試使用它們時,缺乏的類型會引發運行時錯誤。ide

  • 在啓動時沒有辦法知道應用程序中使用的某些類型已經丟失。還能夠包含錯誤的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中的代碼,沒有任何限制,除了在訪問控制方面由類型聲明指定的代碼。

類路徑上的JAR,由Java運行時加載和訪問

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代碼,模塊能夠看作零個或多個包的集合。 下圖顯示了三個名爲policyclaimutility的模塊,其中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指的是兩個模塊。

下圖描述了兩個名爲policyclaim的模塊之間的依賴關係。 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選項指定要運行的模塊及其主類。

繼續介紹policyclaim模塊的例子,假設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.sql模塊依賴的模塊圖

下圖顯示了java.se的平臺模塊的最複雜的模塊圖形之一。 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模塊中的內容

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 -hjava -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命令列出運行時可用的可觀察模塊。 還可使用此選項打印模塊的描述。

相關文章
相關標籤/搜索