Java 9 揭秘(10. 模塊API)


Tips
做一個終身學習的人。

Java 9

在本章節中,主要介紹以下內容:

  • 什么是模塊 API
  • 如何在程序中表示模塊和模塊描述
  • 如何讀取程序中的模塊描述
  • 如何表示模塊的版本
  • 如何使用ModuleModuleDescriptor類讀取模塊的屬性
  • 如何使用Module類在運行時更新模塊的定義
  • 如何創建可用於模塊的注解以及如何讀取模塊上使用的注解
  • 什么是模塊層和配置
  • 如何創建自定義模塊層並將模塊加載到它們中

一. 什么是模塊API

模塊API由可以讓你對模塊進行編程訪問的類和接口組成。 使用API,可以通過編程方式:

  • 讀取,修改和構建模塊描述符
  • 加載模塊
  • 讀取模塊的內容
  • 搜索加載的模塊
  • 創建新的模塊層

模塊API很小。 它由大約15個類和接口組成,分布在兩個包中:

  • java.lang
  • java.lang.module

ModuleModuleLayerLayerInstantiationException類在java.lang包中,其余的在java.lang.module包中。 下表包含模塊API中的類的列表,每個類的簡要說明。 列表未排序。 首先列出了ModuleModuleDescriptor,因為應用程序開發人員最常使用它們。 所有其他類通常由容器和類庫使用。 該列表不包含Module API中的異常類。

描述
Module 表示運行時模塊。
ModuleDescriptor 表示模塊描述。 這是不可變類。
ModuleDescriptor.Builder 用於以編程方式構建模塊描述的嵌套構建器類。
ModuleDescriptor.Exports 表示模塊聲明中的exports語句的嵌套類。
ModuleDescriptor.Opens 表示模塊聲明中的opens語句的嵌套類。
ModuleDescriptor.Provides 表示模塊聲明中的provides語句的嵌套類。
ModuleDescriptor.Requires 表示模塊聲明中的requires語句的嵌套類。
ModuleDescriptor.Version 表示模塊版本字符串的嵌套類。 它包含一個從版本字符串返回其實例的parse(String v)工廠方法。
ModuleDescriptor.Modifier 枚舉類,其常量表示在模塊聲明中使用的修飾符,例如打開模塊的OPEN
ModuleDescriptor.Exports.Modifier 枚舉類,其常量表示在模塊聲明中用於exports語句的修飾符。
ModuleDescriptor.Opens.Modifier 枚舉類,其常量表示在模塊聲明中的opens語句上使用的修飾符。
ModuleDescriptor.Requires.Modifier 枚舉類,其常量表示在模塊聲明中的requires語句上使用的修飾符。
ModuleReference 模塊的內容的引用。 它包含模塊的描述及其位置。
ResolvedModule 表示模塊圖中已解析的模塊。 包含模塊的名稱,其依賴關系和對其內容的引用。 它可以用於遍歷模塊圖中模塊的所有傳遞依賴關系。
ModuleFinder 用於在指定路徑或系統模塊上查找模塊的接口。 找到的模塊作為ModuleReference的實例返回。 它包含工廠方法來獲取它的實例。
ModuleReader 用於讀取模塊內容的接口。 可以從ModuleReference獲取ModuleReader
Configuration 表示解析模塊的模塊圖。
ModuleLayer 包含模塊圖(Configuration)以及模塊圖中的模塊與類加載器之間的映射。
ModuleLayer.Controller 用於控制ModuleLayer中的模塊的嵌套類。 ModuleLayer類中的方法返回此類的實例。

二. 表示模塊

Module類的實例代表一個運行時模塊。 加載到JVM中的每個類型都屬於一個模塊。JDK 9在Class類中添加了一個名為getModule()的方法,該類返回該類所屬的模塊。 以下代碼片段顯示了如何獲取BasicInfo的類的模塊:

// Get the Class object for of the BasicInfo class
Class<BasicInfo> cls = BasicInfo.class;
// Get the module reference
Module module = cls.getModule();

模塊可以是命名或未命名的。 Module類的isNamed()方法對於命名模塊返回true,對於未命名的模塊返回false。

每個類加載器都包含一個未命名的模塊,其中包含類加載器從類路徑加載的所有類型。 如果類加載器從模塊路徑加載類型,則這些類型屬於命名模塊。 Class類的getModule()方法可能會返回一個命名或未命名的模塊。 JDK 9將一個名為getUnnamedModule()的方法添加到ClassLoader類中,該類返回類加載器的未命名模塊。 在下面的代碼片段中,假設BasicInfo類是從類路徑加載的,m1m2指的是同一個模塊:

Class<BasicInfo> cls = BasicInfo.class;
Module m1 = cls.getClassLoader().getUnnamedModule();
Module m2 = cls.getModule();

Module類的getName()方法返回模塊的名稱。 對於未命名的模塊,返回null。

// Get the module name
String moduleName = module.getName();

Module類中的getPackages()方法返回包含模塊中所有包的Set<String>類型。getClassLoader()方法返回模塊的類加載器。

getLayer()方法返回包含該模塊的ModuleLayer; 如果模塊不在圖層中,則返回null。 模塊層僅包含命名模塊。 所以,這個方法總是為未命名的模塊返回null。

三. 描述模塊

ModuleDescriptor類的實例表示一個模塊定義,它是從一個模塊聲明創建的 —— 通常來自module-info.class文件。 模塊描述也可以使用ModuleDescriptor.Builder類創建。 可以使用命令行選項來擴充模塊聲明,例如--add-reads--add-exports-add-opens,並使用Module類中的方法,如addReads()addOpens()addExports()ModuleDescriptor表示在模塊聲明時添加的模塊描述,而不是增強的模塊描述。 Module類的getDescriptor()方法返回一個ModuleDescriptor

Class<BasicInfo> cls = BasicInfo.class;
Module module = cls.getModule();
// Get the module descriptor
ModuleDescriptor desc = module.getDescriptor();

Tips
ModuleDescriptor是不可變的。 未命名的模塊沒有模塊描述。 Module類的getDescriptor()方法為未命名的模塊返回null。

還可以使用ModuleDescriptor類的靜態read()方法從module-info.class文件讀取模塊聲明的二進制形式來創建一個ModuleDescriptor對象。 以下代碼片段從當前目錄中讀取一個module-info.class文件。 為清楚起見排除異常處理:

String moduleInfoPath = "module-info.class";
ModuleDescriptor desc = ModuleDescriptor.read(new FileInputStream(moduleInfoPath));

四. 表示模塊聲明

ModuleDescriptor類包含以下靜態嵌套類,其實例表示模塊聲明中具有相同名稱的語句:

  • ModuleDescriptor.Exports
  • ModuleDescriptor.Opens
  • ModuleDescriptor.Provides
  • ModuleDescriptor.Requires

請注意,沒有ModuleDescriptor.Uses類來表示uses語句。 這是因為uses語句可以表示為String的服務接口名稱。

五. 表示exports語句

ModuleDescriptor.Exports類的實例表示模塊聲明中的exports語句。 類中的以下方法返回導出語句的組件:

  • boolean isQualified()
  • Set<ModuleDescriptor.Exports.Modifier> modifiers()
  • String source()
  • Set targets()

isCualified()方法對於限定的導出返回true,對於非限定的導出,返回false。 source()方法返回導出的包的名稱。 對於限定的導出,targets()方法返回一個不可變的模塊名稱set類型,導出該包,對於非限定的導出,它返回一個空的setmodifiers()方法返回一系列exports語句的修飾符,它們是ModuleDescriptor.Exports.Modifier枚舉的常量,它包含以下兩個常量:

  • MANDATED:源模塊聲明中的exports隱式聲明。
  • SYNTHETIC:源模塊聲明中的exports未明確或隱含地聲明。

六. 表示opens語句

ModuleDescriptor.Opens類的實例表示模塊聲明中的一個opens語句。 類中的以下方法返回了opens語句的組件:

  • boolean isQualified()
  • Set<ModuleDescriptor.Opens.Modifier> modifiers()
  • String source()
  • Set targets()

isCualified()方法對於限定的打開返回true,對於非限定打開,返回false。 source()方法返回打開包的名稱。 對於限定的打開,targets()方法返回一個不可變的模塊名稱set類型,打開該包,對於非限定打開,它返回一個空set。 該modifiers()方法返回一系列的opens語句,它們是嵌套的ModuleDescriptor.Opens.Modifier枚舉的常量,它包含以下兩個常量:

  • MANDATED:源模塊聲明的中的opens隱式聲明。
  • SYNTHETIC:源模塊聲明中的opens未明確或隱含地聲明。

七. 表示provides語句

ModuleDescriptor.Provides類的實例表示模塊聲明中特定服務類型的一個或多個provides語句。 以下兩個provides語句為相同的服務類型X.Y指定兩個實現類:

provides X.Y with A.B;
provides X.Y with Y.Z;

ModuleDescriptor.Provides類的實例將代表這兩個語句。 類中的以下方法返回了provides語句的組件:

  • List providers()
  • String service()

providers()方法返回提供者類的完全限定類名的列表。 在上一個示例中,返回的列表將包含A.BY.Zservice()方法返回服務類型的全限定名稱。 在前面的例子中,它將返回X.Y.

八. 表示requires語句

ModuleDescriptor.Requires類的實例表示模塊聲明中的requires語句。 類中的以下方法返回requires語句的組件:

  • Optional<ModuleDescriptor.Version> compiledVersion()
  • Optional rawCompiledVersion()
  • String name()
  • Set<ModuleDescriptor.Requires.Modifier> modifiers()

假設一個名為M的模塊有一個requires N語句被編譯。如果N的模塊版本在編譯時可用,則該版本將記錄在M的模塊描述中。compiledVersion()方法返回N中的Optional版本。如果N的版本沒有可用,則該方法返回一個空可選。在requires語句中指定的模塊的模塊版本僅在信息方面被記錄在模塊描述中。模塊系統在任何階段都不使用它。但是,它可以被工具和框架用於診斷目的。例如,一個工具可以驗證使用requires語句指定為依賴關系的所有模塊必須具有與編譯期間記錄的相同或更高版本的版本。

繼續前一段中的示例,rawCompiledVersion()方法返回Optional<String>中的模塊N的版本。在大多數情況下,compileVersion()rawCompiledVersion()的兩個方法將返回相同的模塊版本,但是可以以兩種不同的格式返回:一個Optional<ModuleDescriptor.Version>對象,另一個Optional<String>對象。可以擁有一個模塊版本無效的模塊。這樣的模塊可以在Java模塊系統之外創建和編譯。可以將具有無效模塊版本的模塊加載為Java模塊。在這種情況下,compileVersion()方法返回一個空的Optional<ModuleDescriptor.Version>,因為模塊版本不能被解析為有效的Java模塊版本,而rawCompiledVersion()返回一個包含無效模塊版本的Optional<String>

Tips
ModuleDescriptor.Requires類的rawCompiledVersion()方法可能返回所需的模塊的不可解析版本。

name()方法返回在requires語句中指定的模塊的名稱。 modifiers()方法返回的是requires語句的一組修飾符,它們是嵌套的ModuleDescriptor.Requires.Modifier枚舉的常量,它包含以下常量:

  • MANDATED:在源模塊聲明的中的依賴關系的隱式聲明。
  • STATIC:依賴關系在編譯時是強制性的,在運行時是可選的。
  • SYNTHETIC:在源模塊聲明中依賴關系的未明確或隱含地聲明。
  • TRANSITIVE:依賴關系使得依賴於當前模塊的任何模塊都具有隱含聲明的依賴於該requires語句命名的模塊。

1. 代表模塊版本

ModuleDescriptor.Version類的實例表示一個模塊的版本。 它包含一個名為parse(String version)的靜態工廠方法,返回其表示指定版本字符串中的版本的實例。 回想一下,你不要在模塊的聲明中指定模塊的版本。 當你將模塊代碼打包到模塊化JAR(通常使用jar工具)時,可以添加模塊版本。 javac編譯器還允許在編譯模塊時指定模塊版本。

模塊版本字符串包含三個組件:

  • 強制版本號
  • 可選的預發行版本
  • 可選構建版本

模塊版本具有以下形式:

vNumToken+ ('-' preToken+)? ('+' buildToken+)?

每個組件是一個token序列;每個都是非負整數或一個字符串。 token由標點符號“,”,“-” 或“+”或從數字序列轉換為既不是數字也不是標點符號的字符序列,反之亦然。版本字符串必須以數字開頭。 版本號是由一系列由“."分隔token序列組成。 以第一個“-”或“+”字符終止。 預發行版本是由一系列由“.”或“-”分隔token序列組成。 以第一個“+”字符終止。 構建版本是由“.”,“,”,“-”或“+”字符分隔的token序列。

ModuleDescriptor類的version()方法返回Optional<ModuleDescriptor.Version>

2. 模塊的其他屬性

在包裝模塊化JAR時,還可以在module-info.class文件中設置其他模塊屬性,如如主類名,操作系統名稱等。ModuleDescriptor類包含返回每個這樣的屬性的方法。ModuleDescriptor類中包含以下令人感興趣的方法:

  • Set<ModuleDescriptor.Exports> exports()
  • boolean isAutomatic()
  • boolean isOpen()
  • Optional mainClass()
  • String name()
  • Set<ModuleDescriptor.Opens> opens()
  • Set packages()
  • Set<ModuleDescriptor.Provides> provides()
  • Optional rawVersion()
  • Set<ModuleDescriptor.Requires> requires()
  • String toNameAndVersion()
  • Set uses()

方法名稱很直觀,以便了解其目的。 下面這兩個方法,需要一些解釋:packages()provide()

ModuleDescriptor類包含一個名為packages()的方法,Module類包含一個名為getPackages()的方法。 兩者都返回包名的集合。 為什么為了同一目的有兩種方法? 事實上,它們有不同的用途。 在ModuleDescripto中,該方法返回在模塊聲明中定義的包名的集合,無論它們是否被導出。 回想一下,你無法獲得一個未命名模塊的ModuleDescriptor,在這種情況下,可以使用Module類中的getPackages()方法在未命名模塊中獲取軟件包名稱。 另一個區別是ModuleDescriptor記錄的包名是靜態的;Module記錄的包名稱是動態的,它記錄在調用getPackages()方法時在模塊中加載的包。 模塊記錄在運行時當前加載的所有包。

provides()方法返回Set<ModuleDescriptor.Provides>,考慮在模塊聲明中以下provides語句:

provides A.B with X.Y1;
provides A.B with X.Y2;
provides P.Q with S.T1;

在這種情況下,該集合包含兩個元素 —— 一個服務類型A.B,一個服務類型P.Q。 一個元素的service()providers()方法分別返回A.BX.Y1X.Y2的列表。 對於另一個元素的這些方法將返回P.Q和包含S.T1的的列表。

3. 了解模塊基本信息

在本節中,將展示如何在運行時讀取有關模塊的基本信息的示例。 下面包含名為com.jdojo.module.api的模塊的模塊聲明。 它讀取三個模塊並導出一個包。 兩個讀取模塊com.jdojo.prime和com.jdojo.intro在前幾章中使用過。 需要將這兩個模塊添加到模塊路徑中進行編譯,並在com.jdojo.module.api模塊中運行代碼。 java.sql模塊是一個JDK模塊。

// module-info.java
module com.jdojo.module.api {
    requires com.jdojo.prime;
    requires com.jdojo.intro;
    requires java.sql;
    exports com.jdojo.module.api;
}

下面包含一個名為ModuleBasicInfo的類的代碼,它使用ModuleModuleDescriptor類打印三個模塊的模塊詳細信息。

// ModuleBasicInfo.java
package com.jdojo.module.api;
import com.jdojo.prime.PrimeChecker;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleDescriptor.Exports;
import java.lang.module.ModuleDescriptor.Provides;
import java.lang.module.ModuleDescriptor.Requires;
import java.sql.Driver;
import java.util.Set;
public class ModuleBasicInfo {
    public static void main(String[] args) {
        // Get the module of the current class
        Class<ModuleBasicInfo> cls = ModuleBasicInfo.class;
        Module module = cls.getModule();
        // Print module info
        printInfo(module);
        System.out.println("------------------");
        // Print module info
        printInfo(PrimeChecker.class.getModule());
        System.out.println("------------------");
        // Print module info
        printInfo(Driver.class.getModule());
    }
    public static void printInfo(Module m) {
        String moduleName = m.getName();
        boolean isNamed = m.isNamed();
        // Print module type and name
        System.out.printf("Module Name: %s%n", moduleName);
        System.out.printf("Named Module: %b%n", isNamed);
        // Get the module descriptor
        ModuleDescriptor desc = m.getDescriptor();
        // desc will be null for unnamed module
        if (desc == null) {
            Set<String> currentPackages = m.getPackages();
            System.out.printf("Packages: %s%n", currentPackages);
            return;
        }
        Set<Requires> requires = desc.requires();
        Set<Exports> exports = desc.exports();
        Set<String> uses = desc.uses();
        Set<Provides> provides = desc.provides();
        Set<String> packages = desc.packages();
        System.out.printf("Requires: %s%n", requires);
        System.out.printf("Exports: %s%n", exports);
        System.out.printf("Uses: %s%n", uses);
        System.out.printf("Provides: %s%n", provides);
        System.out.printf("Packages: %s%n", packages);
    }
}

我們以模塊模式和傳統模式運行ModuleBasicInfo類。 以下命令將使用模塊模式:

C:\Java9Revealed>java --module-path com.jdojo.module.api\dist;com.jdojo.prime\dist;com.jdojo.intro\dist
--module com.jdojo.module.api/com.jdojo.module.api.ModuleBasicInfo

輸出結果為:

Module Name: com.jdojo.module.api
Named Module: true
Requires: [mandated java.base (@9-ea), com.jdojo.intro, java.sql (@9-ea), com.jdojo.prime]
Exports: [com.jdojo.module.api]
Uses: []
Provides: []
Packages: [com.jdojo.module.api]
------------------
Module Name: com.jdojo.prime
Named Module: true
Requires: [mandated java.base (@9-ea)]
Exports: [com.jdojo.prime]
Uses: [com.jdojo.prime.PrimeChecker]
Provides: []
Packages: [com.jdojo.prime]
------------------
Module Name: java.sql
Named Module: true
Requires: [transitive java.logging, transitive java.xml, mandated java.base]
Exports: [javax.transaction.xa, java.sql, javax.sql]
Uses: [java.sql.Driver]
Provides: []
Packages: [javax.sql, java.sql, javax.transaction.xa]
Now let’s run the ModuleBasicInfo class in legacy mode by using the class path as follows:
C:\Java9Revealed>java -cp com.jdojo.module.api\dist\com.jdojo.module.api.jar;com.jdojo.prime\dist\com.jdojo.prime.jar com.jdojo.module.api.ModuleBasicInfo
Module Name: null
Named Module: false
Packages: [com.jdojo.module.api]
------------------
Module Name: null
Named Module: false
Packages: [com.jdojo.module.api, com.jdojo.prime]
------------------
Module Name: java.sql
Named Module: true
Requires: [mandated java.base, transitive java.logging, transitive java.xml]
Exports: [javax.transaction.xa, javax.sql, java.sql]
Uses: [java.sql.Driver]
Provides: []
Packages: [java.sql, javax.transaction.xa, javax.sql]

第二次運行,ModuleBasicInfoPrimeChecker類被加載到應用程序類加載器的未命名模塊中,這反映在為兩個模塊isNamed()方法返回false。 注意Module類的getPackages()方法的動態特性。 當第一次調用它時,它只返回一個包名稱com.jdojo.module.api。 當它第二次被調用時,它返回兩個包名稱com.jdojo.module.api和com.jdojo.prime。 這是因為未命名模塊中的包是從新的包中添加的類型加載到未命名的模塊中。 在這兩種情況下,java.sql模塊的輸出保持不變,因為平台類型始終加載到同一模塊中,而與運行java啟動的模式無關。

九. 查詢模塊

針對模塊運行的典型查詢包括:

  • 模塊M可以讀另一個模塊N嗎?
  • 模塊可以使用特定類型的服務嗎?
  • 模塊是否將特定包導出到所有或某些模塊?
  • 一個模塊是否打開一個特定的包到所有或一些模塊?
  • 這個模塊是命名還是未命名模塊?
  • 這是一個自動命名模塊嗎?
  • 這是一個開放模塊嗎?

可以使用命令行選項擴充模塊描述,並以編程方式使用Module API。 可以將模塊屬性的所有查詢分為兩類:在加載模塊后,其結果可能會更改的查詢,以及在模塊加載后其結果不會更改的查詢。 Module類包含第一類中查詢的方法,ModuleDescriptor類包含第二類中查詢的方法。Module類為第一類中的查詢提供了以下方法:

  • boolean canRead(Module other)
  • boolean canUse(Class<?> service)
  • boolean isExported(String packageName)
  • boolean isExported(String packageName, Module other)
  • boolean isOpen(String packageName)
  • boolean isOpen(String packageName, Module other)
  • boolean isNamed()

方法名稱直觀足夠告訴你他們做了什么。 isNamed()方法對於命名模塊返回true,對於未命名的模塊返回false。 名稱或未命名的模塊類型在模塊加載完成后不會更改。 此方法在Module類中提供,因為無法獲取未命名模塊的ModuleDescriptor

ModuleDescriptor包含三種方法來告訴你模塊的類型以及模塊描述符的生成方式。 如果isOpen()方法是一個打開的模塊,則返回true,否則返回false。isAutomatic()方法對於自動命名模塊返回true,否則返回false。

下面包含名QueryModule類的代碼,它是com.jdojo.module.api模塊的成員。 它顯示如何查詢模塊的依賴關系檢查,以及軟件包是導出還是打開到所有模塊或僅對特定模塊。

// QueryModule.java
package com.jdojo.module.api;
import java.sql.Driver;
public class QueryModule {
    public static void main(String[] args) throws Exception {
        Class<QueryModule> cls = QueryModule.class;
        Module m = cls.getModule();
        // Check if this module can read the java.sql module
        Module javaSqlModule = Driver.class.getModule();
        boolean canReadJavaSql = m.canRead(javaSqlModule);
        // Check if this module exports the com.jdojo.module.api package to all modules
        boolean exportsModuleApiPkg =  m.isExported("com.jdojo.module.api");
        // Check if this module exports the com.jdojo.module.api package to java.sql module
        boolean exportsModuleApiPkgToJavaSql =
                m.isExported("com.jdojo.module.api", javaSqlModule);
        // Check if this module opens the com.jdojo.module.api package to java.sql module
        boolean openModuleApiPkgToJavaSql = m.isOpen("com.jdojo.module.api", javaSqlModule);
        // Print module type and name
        System.out.printf("Named Module: %b%n", m.isNamed());
        System.out.printf("Module Name: %s%n", m.getName());
        System.out.printf("Can read java.sql? %b%n", canReadJavaSql);
        System.out.printf("Exports com.jdojo.module.api? %b%n", exportsModuleApiPkg);
        System.out.printf("Exports com.jdojo.module.api to java.sql? %b%n",
                exportsModuleApiPkgToJavaSql);
        System.out.printf("Opens com.jdojo.module.api to java.sql? %b%n",
                openModuleApiPkgToJavaSql);
    }
}

輸出結果為:

Named Module: true
Module Name: com.jdojo.module.api
Can read java.sql? true
Exports com.jdojo.module.api? true
Exports com.jdojo.module.api to java.sql? true
Opens com.jdojo.module.api to java.sql? false

十. 更新模塊

在前幾章中,了解了如何使用--add-exports--add-opened--add-reads命令行選項向模塊添加導出和讀取。 在本節中,展示如何以編程方式將這些語句添加到模塊中。 Module類包含以下方法,可以在運行時修改模塊聲明:

  • Module addExports(String packageName, Module other)
  • Module addOpens(String packageName, Module other)
  • Module addReads(Module other)
  • Module addUses(Class<?> serviceType)

使用命令行選項和上面的種方法來修改模塊的聲明有很大的區別。 使用命令行選項,可以修改任何模塊的聲明。 然而,這些方法是調用者敏感的。 調用這些方法的代碼必須在聲明被修改的模塊中,除了調用addOpens()方法。 也就是說,如果無法訪問模塊的源代碼,則無法使用這些方法來修改該模塊的聲明。 這些方法通常被框架使用,可以適應運行時需要與其他模塊交互。

所有這些方法在處理命名模塊時都會拋出IllegalCallerException,因此調用者不允許調用這些模塊。

addExports()方法更新模塊以將指定的包導出到指定的模塊。 如果指定的包已經導出或打開到指定的模塊,或者在未命名或打開的模塊上調用該方法,則調用該方法將不起作用。 如果指定的包為空或模塊中不存在,則拋出IllegalArgumentException異常。 調用此方法與向模塊聲明中添加限定導出具有相同的效果:

exports <packageName> to <other>;

addOpens()方法與addExports()方法工作方式相同,只是它更新模塊以將指定的包打開到指定的模塊。 它類似於在模塊中添加以下語句:

opens <packageName> to <other>;

addOpens()方法對關於誰可以調用此方法的規則會產生異常。 可以從同一模塊的代碼調用其他方法。 但是,可以從另一個模塊的代碼調用一個模塊的addOpens()方法。 假設模塊M使用以下聲明將軟件包P對模塊N開放:

module M {
    opens P to N;
}

在這種情況下,模塊N被允許調用模塊M上的addOpens(“P”, S)方法,這允許模塊N將軟件包P打開到模塊S。當模塊的作者可以將模塊的包打開到已知的抽象框架模塊時,在模塊運行時發現並使用另一個實現模塊。動態已知的模塊都可能需要對所聲明的模塊進行深層反射訪問。在這種情況下,模塊的作者只需要了解抽象框架的模塊名稱並打開它的包。在運行時,抽象框架的模塊可以打開與動態發現的實現模塊相同的包。考慮JPA作為一個抽象框架,定義了一個java.persistence模塊,並在運行時發現了其他JPA實現,如Hibernate和EclipseLink。在這種情況下,模塊的作者只能打開一個包到java.persistence模塊,該模塊可以在運行時打開與Hibernate或EclipseLink模塊相同的軟件包。

addReads()方法將可讀性邊界從該模塊添加到指定的模塊。 如果指定的模塊本身是因為每個模塊都可以讀取自身或者由於未命名模塊可以讀取所有模塊而在未命名模塊上被調用,則此方法無效。 調用此方法與requires語句添加到模塊聲明中的作用相同:

requires <other>;

addUses()方法更新模塊以添加服務依賴關系,因此可以使用ServiceLoader類來加載指定服務類型的服務。 在未命名或自動命名模塊上調用時不起作用。 其效果與在模塊聲明中添加以下uses語句相同:

uses <serviceType>;

下面包含UpdateModule類的代碼。 它在com.jdojo.module.api模塊中。 請注意,模塊聲明不包含uses語句。 該類包含一個findFirstService()方法,它接受一個服務類型作為參數。 它檢查模塊是否可以加載服務類型。 回想一下,模塊必須包含具有指定服務類型的uses語句,以使用ServiceLoader類加載該服務類型。 該方法使用Module類的addUses()方法,如果不存在,則為該服務類型添加一個uses語句。 最后,該方法加載並返回加載的第一個服務提供者。

// UpdateModule.java
package com.jdojo.module.api;
import java.util.ServiceLoader;
public class UpdateModule {
    public static <T> T findFirstService(Class<T> service) {
        /* Before loading the service providers, check if this module can use (or load) the
           service. If not, update the module to use the service.
        */
        Module m = UpdateModule.class.getModule();
        if (!m.canUse(service)) {
            m.addUses(service);
        }
        return ServiceLoader.load(service)
                            .findFirst()
                            .orElseThrow(
         () -> new RuntimeException("No service provider found for the service: " +
                                    service.getName()));
    }
}

現在將測試UpdateModule類的findFirstService()方法。 下面包含名為com.jdojo.module.api.test的模塊的聲明。 它聲明對com.jdojo.prime模塊的依賴,因此它可以使用PrimeChecker服務類型接口。 它聲明對com.jdojo.module.api模塊的依賴,因此它可以使用UpdateModule類加載服務。 需要將這兩個模塊添加到NetBeans中com.jdojo.module.api.test模塊的模塊路徑中。

// module-info.java
module com.jdojo.module.api.test {
    requires com.jdojo.prime;
    requires com.jdojo.module.api;
}

下面包含com.jdojo.module.api.test模塊中的Main類的代碼。

// Main.java
package com.jdojo.module.api.test;
import com.jdojo.module.api.UpdateModule;
import com.jdojo.prime.PrimeChecker;
public class Main {
    public static void main(String[] args) {
        long[] numbers = {3, 10};
        try {
            // Obtain a service provider for the com.jdojo.prime.PrimeChecker service type
            PrimeChecker pc = UpdateModule.findFirstService(PrimeChecker.class);
            // Check a few numbers for prime
            for (long n : numbers) {
                boolean isPrime = pc.isPrime(n);
                System.out.printf("%d is a prime: %b%n", n, isPrime);
            }
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());
        }
    }
}

使用以下命令運行Main類。 確保將com.jdojo.intro模塊添加到模塊路徑,因為com.jdojo.module.api.test模塊讀取com.jdojo.module.api模塊,該模塊讀取com.jdojo.intro模塊。

C:\Java9Revealed>java --module-path com.jdojo.prime\dist;com.jdojo.intro\dist;com.jdojo.module.api\dist;com.jdojo.module.api.test\dist
--module com.jdojo.module.api.test/com.jdojo.module.api.test.Main

輸出結果為:

No service provider found for the service: com.jdojo.prime.PrimeChecker

輸出顯示此程序的正常執行。 這在輸出中指示,它沒有在模塊路徑上找到com.jdojo.prime.PrimeChecker服務類型的服務提供者。 我們為模塊路徑上的com.jdojo.prime.PrimeChecker服務類型添加一個服務提供者com.jdojo.prime.generic模塊,並重新運行程序。 如果你向模塊路徑添加了不同的服務提供者,則可能會得到不同的輸出。

C:\Java9Revealed>java --module-path com.jdojo.prime\dist;com.jdojo.intro\dist;com.jdojo.module.api\dist;com.jdojo.module.api.test\dist;com.jdojo.prime.generic\dist
--module com.jdojo.module.api.test/com.jdojo.module.api.test.Main

輸出結果為:

3 is a prime: true
10 is a prime: false

十一. 訪問模塊資源

模塊可能包含資源,如圖像,音頻/視頻剪輯,屬性文件和策略文件。 模塊中的類文件(.class文件)也被視為資源。Module類包含getResourceAsStream()方法來使用資源名稱來檢索資源:

InputStream getResourceAsStream(String name) throws IOException

十二. 模塊注解

可以在模塊聲明上使用注解。 java.lang.annotation.ElementType枚舉有一個名為MODULE的新值。 如果在注解聲明中使用MODULE作為目標類型,則允許在模塊上使用注解。 在Java 9中,兩個注釋java.lang.Deprecatedjava.lang.SuppressWarnings已更新為在模塊聲明中使用。 它們可以使用如下:

@Deprecated(since="1.2", forRemoval=true)
@SuppressWarnings("unchecked")
module com.jdojo.myModule {
    // Module statements go here
}

當模塊被棄用時,使用該模塊需要但不在導出或打開語句中,將導致發出警告。 該規則基於以下事實:如果模塊M不推薦使用,則使用需要M的模塊的用戶獲得棄用警告。 諸如導出和打開的其他語句在被棄用的模塊中。 不建議使用的模塊不會對模塊中的類型的使用發出警告。 類似地,如果在模塊聲明中抑制了警告,則抑制應用於模塊聲明中的元素,而不適用於該模塊中包含的類型。

Module類實現java.lang.reflect.AnnotatedElement接口,因此可以使用各種與注解相關的方法來讀取它們。 要在模塊聲明中使用的注解類型必須包含ElementType.MODULE作為目標。

Tips
不能對各個模塊語句添加注解。 例如,不能使用@Deprecated注解用在exports語句,表示導出的包將在以后的版本中被刪除。 在早期的設計階段,它是經過考慮和拒絕的,理由是這個功能將需要大量的時間,這是不需要的。 如果需要,可以在將來添加。 因此,將不會在ModuleDescriptor類中找到任何與注解相關的方法。

現在我們創建一個新的注解類型,並在模塊聲明中使用它。 如下包含一個名為com.jdojo.module.api.annotation的模塊的模塊聲明,該模塊包含三個注解。

// module-info.java
import com.jdojo.module.api.annotation.Version;
@Deprecated(since="1.2", forRemoval=false)
@SuppressWarnings("unchecked")
@Version(major=1, minor=2)
module com.jdojo.module.api.annotation {
    // No module statements
}

版本注解類型已在同一模塊中聲明,其源代碼如下所示。 新注解類型的保留策略是RUNTIME

// Version.java
package com.jdojo.module.api.annotation;
import static java.lang.annotation.ElementType.MODULE;
import static java.lang.annotation.ElementType.PACKAGE;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
@Retention(RUNTIME)
@Target({PACKAGE, MODULE, TYPE})
public @interface Version {
    int major();
    int minor();
}

下面包含了一個AnnotationTest類的代碼。 它讀取com.jdojo.module.api.annotation模塊上的注解。 輸出不包含模塊上存在的@SuppressWarnings注解,因為此注解使用RetentionPolicy.RUNTIME的保留策略,這意味着注解不會在運行時保留。

// AnnotationTest.java
package com.jdojo.module.api.annotation;
import java.lang.annotation.Annotation;
public class AnnotationTest {
    public static void main(String[] args) {
        // Get the module reference of the com.jdojo.module.api.annotation module
        Module m = AnnotationTest.class.getModule();
        // Print all annotations
        Annotation[] a = m.getAnnotations();
        for(Annotation ann : a) {
            System.out.println(ann);
        }
        // Read the Deprecated annotation
        Deprecated d = m.getAnnotation(Deprecated.class);
        if (d != null) {
            System.out.printf("Deprecated: since=%s, forRemoval=%b%n",
                              d.since(), d.forRemoval());
        }
        // Read the Version annotation
        Version v = m.getAnnotation(Version.class);
        if (v != null) {
            System.out.printf("Version: major=%d, minor=%d%n", v.major(), v.minor());
        }
    }
}

輸出結果為:

@java.lang.Deprecated(forRemoval=false, since="1.2")
@com.jdojo.module.api.annotation.Version(major=1, minor=2)
Deprecated: since=1.2, forRemoval=false
Version: major=1, minor=2

十三. 加載類

可以使用Class類的以下靜態forName()方法來加載和初始化一個類:

  • Class<?> forName(String className) throws ClassNotFoundException
  • Class<?> forName(String className, boolean initialize, ClassLoader loader) throws ClassNotFoundException
  • Class<?> forName(Module module, String className)

在這些方法中,className參數是要加載的類或接口的完全限定名稱,例如java.lang.Threadcom.jdojo.intro.Welcome。 如果initialize參數為true,則該類將被初始化。
The forName(String className)方法在加載之后初始化該類,並使用當前的類加載器,該加載器是加載調用此方法的類的類加載器。 表達式Class.forName("P.Q")里的實例方法相當於Class.forName("P.Q", true, this.getClass().getClassLoader())

下面包含作為com.jdojo.module.api模塊成員的LoadClass類的代碼。 該類包含兩個版本的loadClass()方法。 該方法加載指定的類,並且在成功加載類之后,它嘗試使用無參構造函數來實例化該類。 請注意,com.jdojo.intro模塊不導出包含Welcome類的com.jdojo.intro包。 此示例嘗試加載和實例化Welcome類和另外兩個不存在的類。

// LoadingClass.java
package com.jdojo.module.api;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Optional;
public class LoadingClass {
    public static void main(String[] args) {
        loadClass("com.jdojo.intro.Welcome");
        loadClass("com.jdojo.intro.XYZ");
        String moduleName = "com.jdojo.intro";
        Optional<Module> m = ModuleLayer.boot().findModule(moduleName);
        if (m.isPresent()) {
            Module introModule = m.get();
            loadClass(introModule, "com.jdojo.intro.Welcome");
            loadClass(introModule, "com.jdojo.intro.ABC");
        } else {
            System.out.println("Module not found: " + moduleName +
             ". Please make sure to add the module to the module path.");
        }
    }
    public static void loadClass(String className) {
        try {
            Class<?> cls = Class.forName(className);
            System.out.println("Class found: " + cls.getName());
            instantiateClass(cls);
        } catch (ClassNotFoundException e) {
            System.out.println("Class not found: " + className);
        }
    }
    public static void loadClass(Module m, String className) {
        Class<?> cls = Class.forName(m, className);
        if (cls == null) {
            System.out.println("Class not found: " + className);
        } else {
            System.out.println("Class found: " + cls.getName());
            instantiateClass(cls);
        }
    }
    public static void instantiateClass(Class<?> cls) {
        try {
            // Get the no-arg constructor
            Constructor<?> c = cls.getConstructor();
            Object o = c.newInstance();
            System.out.println("Instantiated class: " + cls.getName());
        } catch (InstantiationException | IllegalAccessException |
                 IllegalArgumentException | InvocationTargetException e) {
            System.out.println(e.getMessage());
        } catch (NoSuchMethodException e) {
            System.out.println("No no-args constructor for class: " + cls.getName());
        }
    }
}

嘗試運行LoadClass類,只需將三個必需的模塊添加到模塊路徑中:

C:\Java9Revealed>java
--module-path com.jdojo.module.api\dist;com.jdojo.prime\dist;com.jdojo.intro\dist
--module com.jdojo.module.api/com.jdojo.module.api.LoadingClass

輸出結果為:

Class found: com.jdojo.intro.Welcome
class com.jdojo.module.api.LoadingClass (in module com.jdojo.module.api) cannot access class com.jdojo.intro.Welcome (in module com.jdojo.intro) because module com.jdojo.intro does not export com.jdojo.intro to module com.jdojo.module.api
Class not found: com.jdojo.intro.XYZ
Class found: com.jdojo.intro.Welcome
class com.jdojo.module.api.LoadingClass (in module com.jdojo.module.api) cannot access class com.jdojo.intro.Welcome (in module com.jdojo.intro) because module com.jdojo.intro does not export com.jdojo.intro to module com.jdojo.module.api
Class not found: com.jdojo.intro.ABC

輸出顯示我們可以加載com.jdojo.intro.Welcome類。 但是,我們無法將其實例化,因為它不會導出到com.jdojo.intro模塊中。 以下命令使用--add-exports選項將com.jdojo.intro模塊中的com.jdojo.intro包導出到com.jdojo.module.api模塊。 輸出顯示我們可以加載並實例化Welcome類。

c:\Java9Revealed>java
--module-path com.jdojo.module.api\dist;com.jdojo.prime\dist;com.jdojo.intro\dist
--add-exports com.jdojo.intro/com.jdojo.intro=com.jdojo.module.api
--module com.jdojo.module.api/com.jdojo.module.api.LoadingClass

輸出結果為:

Class found: com.jdojo.intro.Welcome
Instantiated class: com.jdojo.intro.Welcome
Class not found: com.jdojo.intro.XYZ
Class found: com.jdojo.intro.Welcome
Instantiated class: com.jdojo.intro.Welcome
Class not found: com.jdojo.intro.ABC

十四. 使用模塊層

使用模塊層是一個高級主題。 典型的Java開發人員不需要直接使用模塊層。 現有的應用程序不會使用模塊層。 如果將應用程序遷移到JDK 9或使用JDK 9開發新的應用程序,無論是否需要,都至少使用一個由JVM在啟動時創建的模塊層。 通常,使用插件或容器架構的應用程序將使用模塊層。

層是一組解析的模塊(一個模塊圖),具有將每個模塊映射到負責加載該模塊中所有類型的類加載器的功能。 解析的模塊集合稱為配置。 可以可視化模塊,類加載器,配置和層之間的關系,如下所示:

  • Configuration = A module graph
  • Module Layer = Configuration + (Module -> Class loader)

模塊排列成層。 層次分層排列。 層除了空層以外還有至少一個父層,顧名思義,該層不包含模塊,主要存在作為引導層的父層。 引導層由啟動時由JVM創建,通過針對一組可觀察模塊解析應用程序的初始模塊(根模塊)。 使用類加載器的加載類型在JDK 9中沒有變化。加載器通常使用父類——第一委托機制的模式,其中將加載類型的請求委托給父進程,而父請求委托給其父進程,直到引導類加載器。 如果父節點中沒有一個加載類型,那么最初收到請求的類加載器就會加載它。 下圖給出了模塊,類裝載器和層的布置方式的示例。

在應用程序中將模塊布置成層的示例

在圖中,從X到Y的箭頭意味着X是Y的父類,其中X和Y可以是類加載器或層。 層是堆放的 —— 空層和引導層是最低的兩層。 我們進一步的討論中忽略引用空層,並將啟動層作為堆棧層中的最低層。 引導層是名為Layer1和Layer2的兩個自定義層的父層。

堆疊中給定層中的模塊可以在其下方的層中讀取模塊。 也就是說,Layer1和Layer2都可以讀取引導層中的模塊。 但是,Layer1無法讀取Layer2中的模塊,因為它們是兄弟層。 引導層也不能讀取Layer1和Layer2中的模塊,因為引導層是它們的父層。 如圖上所示,兩個用戶定義的層中的類加載器都將應用程序類加載器作為其父類,這通常是這種情況。 使應用程序類加載器成為自定義類加載器的父級,確保后者能夠讀取引導層中模塊中的所有類型。 當模塊在一層讀取下一層模塊時,模塊的可讀性屬性受到重視。

允許將模塊布置成層次可用於兩個用例(覆蓋機制和擴展機制),這些機制和擴展機制通常在高級Java應用程序(例如作為托管應用程序容器的Java EE應用程序/ Web服務器)中遇到。 在覆蓋機制中,托管應用程序需要覆蓋容器提供的功能,例如使用同一模塊的不同版本。 在擴展機制中,托管應用程序需要補充容器提供的功能,例如提供其他服務提供者。 在上圖中,com.jdojo.test模塊位於引導層以及Layer1中。 這是覆蓋模塊的情況。 Layer1中的模塊版本將被Layer1使用,而Layer2將使用引導層中的該模塊的版本。

通常需要容器允許托管應用程序提供自己的一組可以覆蓋容器中嵌入的模塊。 這可以通過將托管應用程序的模塊加載到容器層頂部的圖層中實現。 加載到特定應用層的模塊將覆蓋服務器級別層中的模塊。 這樣,可以在同一個JVM中使用同一模塊的多個版本。

托管應用程序可能希望使用與容器提供的不同的服務提供者。 通過將應用程序特定的服務提供程序模塊添加到容器層頂部的圖層可以實現。 可以使用ServiceLoader類的load(ModuleLayer layer, Class<S> service) 方法來加載服務提供者。 指定的層將是托管的應用程序特定層。 此方法從指定的層及其父層加載服務提供者。

Tips
層是不可變的。 創建圖層后,無法向其中添加模塊或從中刪除模塊。 如果需要添加模塊或替換其他版本的模塊,則必須拆除圖層並重新創建。

創建圖層是一個多步驟的過程。 需要:

  • 創建模塊查找器
  • 創建一組根模塊
  • 創建配置對象
  • 創建一個圖層

創建圖層后,可以使用它來加載類型。 將在下一節詳細介紹這些步驟。 最后,展示多個版本的模塊如何使用圖層。

1. 查找模塊

模塊查找器是ModuleFinder接口的一個實例。 它用於在模塊解析和服務綁定期間查找ModuleReferences。 該接口包含兩種工廠方法來創建模塊查找器:

  • static ModuleFinder of(Path... entries)
  • static ModuleFinder ofSystem()

of()方法通過搜索指定的路徑序列來定位模塊,這些路徑可以是目錄或打包模塊的路徑。 該方法首先發現模塊名稱按順序搜索指定的路徑。 以下代碼片段顯示了如何創建一個在C:\Java9Revealed\lib和C:\Java9Revealed\customLib目錄中搜索模塊的模塊查找器:

// Create the module paths
Path mp1 = Paths.get("C:\\Java9Revealed\\lib");
Path mp2 = Paths.get("C:\\Java9Revealed\\customLib");
// Create a module finder using two module paths
ModuleFinder finder = ModuleFinder.of(mp1, mp2);

有時候,需要一個ModuleFinder引用,例如傳遞給一個方法,但該模塊查找器不需要查找任何模塊。 可以使用ModuleFinder.of()方法,而不需要任何路徑作為參數創建,例如模塊查找器。

ofSystem()方法返回一個模塊查找器,它可以查找鏈接到運行時的系統模塊。 該方法始終找到java.base模塊。 請注意,可以將自定義的一組模塊鏈接到運行時映像,這意味着使用此方法定位的模塊取決於運行時映像。 自定義運行時映像包含JDK模塊以及應用程序模塊。 該方法將找到兩種類型的模塊。

還可以使用compose()方法從零個更多的模塊查找器的序列中組成一個模塊查找器:

static ModuleFinder compose(ModuleFinder... finders)

該模塊查找器將按照指定的順序使用每個模塊查找器。 第二個模塊查找器將找到第一個模塊查找器未找到的所有模塊,第三個模塊查找器將找到第一個和第二個模塊查找器未找到的所有模塊,依此類推。

ModuleFinder接口包含以下方法來查找模塊:

  • Optional find(String name)
  • Set findAll()

find()方法查找具有指定名稱的模塊。 findAll()方法查找發現者可以找到的所有模塊。

以下包含FindingModule類的代碼,顯示如何使用ModuleFinder。 代碼在Windows上使用路徑,如C:\Java9Revealed\lib,此目錄存儲模塊。 你可能需要在運行該類之前更改模塊路徑。 該類是com.jdojo.module.api模塊的成員。 可能會得到不同的輸出。

// FindingModule.java
package com.jdojo.module.api;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.Set;
public class FindingModule {
    public static void main(String[] args) {
        // Create module paths
        Path mp1 = Paths.get("C:\\Java9Revealed\\lib");
        Path mp2 = Paths.get("C:\\Java9Revealed\\customLib");
        // Create a module finder
        ModuleFinder finder = ModuleFinder.of(mp1, mp2);
        // Find all modules that this finder can locate
        Set<ModuleReference> moduleRefs = finder.findAll();
        // Print the details of the modules found
        moduleRefs.forEach(FindingModule::printInfo);
    }
    public static void printInfo(ModuleReference mr) {
        ModuleDescriptor md = mr.descriptor();
        Optional<URI> location = mr.location();
        URI uri = null;
        if(location.isPresent()) {
            uri = location.get();
        }
        System.out.printf("Module: %s, Location: %s%n", md.name(), uri);
    }
}

輸出結果為:

Module: com.jdojo.prime.probable, Location: file:///C:/Java9Revealed/lib/com.jdojo.prime.probable.jar
Module: com.jdojo.person, Location: file:///C:/Java9Revealed/lib/com.jdojo.person.jar
Module: com.jdojo.address, Location: file:///C:/Java9Revealed/lib/com.jdojo.address.jar
...

2. 讀取模塊內容

在上一節中,學習了如何使用ModuleFinder查找模塊引用,它是ModuleReference類的實例。 ModuleReference封裝了ModuleDescriptor和模塊的位置。 可以使用ModuleReference類的open()方法來獲取ModuleReader接口的實例。 ModuleReader用於列出,查找和讀取模塊的內容。 以下代碼片段顯示了如何獲取java.base模塊的ModuleReader

// Create a system module finder
ModuleFinder finder = ModuleFinder.ofSystem();
// The java.base module is guaranteed to exist
Optional<ModuleReference> omr = finder.find("java.base");
ModuleReference moduleRef = omr.get();
// Get a module reader
ModuleReader reader = moduleRef.open();

ModuleReference類的open()方法拋出一個IOException異常。 在這段代碼中省略了異常處理,以保持代碼簡單。

ModuleReader中的以下方法用於處理模塊的內容。 方法名稱足夠直觀地告訴你他們做了什么。

  • void close() throws IOException
  • Optional find(String resourceName) throws IOException
  • Stream list() throws IOException
  • default Optional open(String resourceName) throws IOException
  • default Optional read(String resourceName) throws IOException
  • default void release(ByteBuffer bb)

傳遞給這些方法的資源名稱是“/”分隔的路徑字符串。 例如,java.base模塊中java.lang.Object類的資源名稱為java/lang/Object.class。

一旦完成了使用ModuleReader,需要使用close()方法關閉它。 如果嘗試使用已經關閉的ModuleReader讀取模塊的內容,則會拋出IOException異常。 read()方法返回一個Optional<ByteBuffer>。 需要調用release(ByteBuffer bb)方法來釋放字節緩沖區,以避免資源泄漏。

下列包含一個程序,顯示如何讀取模塊的內容。 它讀取ByteBufferObject對象的內容,並以字節為單位打印其大小。 它還在java.base模塊中打印五個資源的名稱。 你可能會得到不同的輸出。

// ReadingModuleContents.java
package com.jdojo.module.api;
import java.io.IOException;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReader;
import java.lang.module.ModuleReference;
import java.nio.ByteBuffer;
import java.util.Optional;
public class ReadingModuleContents {
    public static void main(String[] args) {
        // Create a system module finder
        ModuleFinder finder = ModuleFinder.ofSystem();
        // The java.base module is guaranteed to exist
        Optional<ModuleReference> omr = finder.find("java.base");
        ModuleReference moduleRef = omr.get();
        // Get a module reader and use it
        try (ModuleReader reader = moduleRef.open()) {
            // Read the Object class and print its size
            Optional<ByteBuffer> bb = reader.read("java/lang/Object.class");
            bb.ifPresent(buffer -> {
                System.out.println("Object.class Size: " + buffer.limit());
                // Release the byte buffer
                reader.release(buffer);
            });
            System.out.println("\nFive resources in the java.base module:");
            reader.list()
                  .limit(5)
                  .forEach(System.out::println);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

輸出結果為:

Object.class Size: 1859
Five resources in the java.base module:
module-info.class
sun/util/BuddhistCalendar.class
sun/util/PreHashedMap$1$1.class
sun/util/PreHashedMap$1.class
sun/util/PreHashedMap$2$1$1.class

4. 創建配置對象

配置表示一組已解析的模塊。 解析的模塊是一個使用requires語句指定的依賴關系的模塊。 模塊解決過程使用兩組模塊:一組根模塊和一組可觀察模塊。 根模塊集合中的每個模塊都用作初始模塊,其requires語句針對可觀察模塊集合進行解析。 根模塊可能需要另一個模塊,這可能需要另一個模塊,等等。 解決過程計算所有根模塊的依賴鏈。 所得到的模塊圖被稱為依賴圖。

依賴圖只考慮了requires語句。 如果一個模塊使用了requires transitive 語句,則依賴於此模塊的模塊將隱含地依賴於在必需傳遞語句中指定的模塊。 依賴關系圖增加了requires transitive 語句模塊的額外可讀性,從而產生一個稱為可讀性圖的模塊圖。

模塊中的usesprovides語句也構成依賴關系。 如果模塊M使用服務類型S,並且另一個模塊N提供T的實現S,則模塊M依賴於使用服務類型S的模塊N。可讀性圖用針對這樣的服務使用依賴性計算的模塊進行擴充。

當創建引導層的配置時,它通過解析依賴關系(requires語句),隱含的可讀性(requires transitive)和服務使用依賴性(usesprovides 語句)來包含模塊。 為用戶定義的層創建配置時,可以選擇包含或排除服務使用依賴關系。

Configuration類的實例表示一個配置。 一個配置至少有一個父類,除了一個空配置。

ResolvedModule類的實例表示配置中已解析的模塊。 它的reads()方法返回一個已解析的模塊讀取的Set<ResolvedModule>configuration()方法返回解析的模塊是其成員的配置。reference()方法返回一個ModuleReference,可以使用它來獲取ModuleReader來讀取模塊的內容。

Configuration類中的以下方法創建一個Configuration對象:

static Configuration empty()
Configuration resolve(ModuleFinder before, ModuleFinder after, Collection<String> roots)
Configuration resolveAndBind(ModuleFinder before, ModuleFinder after, Collection<String> roots)
static Configuration resolve(ModuleFinder before, List<Configuration> parents, ModuleFinder after, Collection<String> roots)
static Configuration resolveAndBind(ModuleFinder before, List<Configuration> parents, ModuleFinder after, Collection<String> roots)

empty()方法返回一個空配置。 這主要用於配置引導層的父配置。

有兩個版本的resolve()resolveAndBind()方法:一個是實例方法,另一個為靜態方法。 他們之間只有一個區別。 實例方法使用當前配置作為父配置來創建新配置,而靜態方法可讓你傳遞新配置的父配置列表。

resolveAndBind()方法的工作方式與resolve()方法相同,只不過它也解決了服務使用依賴關系。 以下代碼片段顯示了如何使用引導層配置作為其父配置來創建配置:

// Define the module finders
String modulePath = "C:\\Java9Revealed\\customLib";
Path path = Paths.get(modulePath);
ModuleFinder beforFinder = ModuleFinder.of(path);
// Our after module finder is empty
ModuleFinder afterFinder = ModuleFinder.of();
// Set up the root modules
Set<String> rootModules = Set.of("com.jdojo.layer");
// Create a configuration using the boot layer’s configuration as its parent configuration
Configuration parentConfig = ModuleLayer.boot().configuration();
Configuration config = parentConfig.resolve(beforFinder, afterFinder, rootModules);

Configuration類中的以下方法用於檢索配置中已解析模塊的詳細信息:

Optional<ResolvedModule> findModule(String name)
Set<ResolvedModule> modules()
List<Configuration> parents()

這些方法的名稱和簽名是直觀的,足以理解它們的使用。 在下一節中,介紹如何使用配置來創建模塊層。

5. 創建模塊層

模塊層是將每個模塊映射到類加載器的配置和功能。 要創建一個圖層,必須先創建一個配置,並有一個或多個類加載器將模塊映射到它們。 模塊的類加載器負責加載該模塊中的所有類型。 可以將配置中的所有模塊映射到一個類加載器;也可以將每個模塊映射到不同的類加載器;或者可以有自定義映射策略。 通常,類加載器使用委派策略來將類加載請求委托給其父類加載器。 當為層中的模塊定義類加載器時,也可以使用此策略。

java.lang包中的ModuleLayer類的實例代表一個模塊層。 該類包含兩個方法,empty()boot(),它們分別返回一個空配置的空層和引導層。 類中的以下方法用於創建自定義圖層:

ModuleLayer defineModules(Configuration cf, Function<String,ClassLoader> clf)
static ModuleLayer.Controller defineModules(Configuration cf, List<ModuleLayer> parentLayers, Function<String,ClassLoader> clf)
ModuleLayer defineModulesWithManyLoaders(Configuration cf, ClassLoader parentClassLoader)
static ModuleLayer.Controller defineModulesWithManyLoaders(Configuration cf, List<ModuleLayer> parentLayers, ClassLoader parentLoader)
ModuleLayer defineModulesWithOneLoader(Configuration cf, ClassLoader parentClassLoader)
static ModuleLayer.Controller defineModulesWithOneLoader(Configuration cf, List<ModuleLayer> parentLayers, ClassLoader parentLoader)

defineModulesXxx()方法有兩個變體:一個集合包含實例方法,另一個集合包含靜態方法。 實例方法使用它們被稱為父層的層,而靜態方法可以指定新層的父層列表。 靜態方法返回一個ModuleLayer.Controller對象,可以使用它來處理新層中的模塊。 ModuleLayer.Controller是java.lang包中的一個嵌套類,具有以下方法:

ModuleLayer.Controller addOpens(Module source, String packageName, Module target)
ModuleLayer.Controller addReads(Module source, Module target)
ModuleLayer layer()

addOpens()addReads()方法可以讓這個層中的一個模塊中的一個包對另一個模塊開放,並將這個層中的模塊的讀取邊界加到另一個模塊。 layer()方法返回該控制器正在管理的ModuleLayer

defineModules(Configuration cf, Function<String,ClassLoader> clf)方法將配置作為其第一個參數。 第二個參數是映射函數,它在配置中獲取模塊名,並為該模塊返回類加載器。 方法調用可能會失敗,如果:

  • 具有相同包的多個模塊映射到同一個類加載器。
  • 一個模塊被映射到定義相同名稱的模塊的類加載器。
  • 模塊被映射到已經在模塊中的任何包中定義了類型的類加載器。

defineModulesWithManyLoaders(Configuration cf, ClassLoader parentClassLoader)方法使用指定的配置創建一個模塊層。 配置中的每個模塊都映射到由此方法創建的不同類加載器。 指定的父類加載器(第二個參數)被設置為通過此方法創建的類加載器的父級。 通常,使用應用程序類加載器作為由此方法創建的所有類加載器的父類加載器。 可以使用null作為第二個參數來使用引導類加載器作為由此方法創建的所有類加載器的父級。 該方法將為配置中的每個模塊創建一個新的類加載器。

defineModulesWithOneLoader(Configuration cf, ClassLoader parentClassLoader)方法使用指定的配置創建一個模塊層。 它使用指定的父類加載器作為其父類創建一個類加載器。 它將配置中的所有模塊映射到該類加載器。 可以使用null作為第二個參數來使用引導類加載器作為由此方法創建的所有類加載器的父級。

以下代碼段創建一個層,引導層作為其父層。 層中的所有模塊將由一個類加載器加載,父類是系統類加載器。

Configuration config = /* create a configuration... */
ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
ModuleLayer parentLayer = ModuleLayer.boot();
ModuleLayer layer = parentLayer.defineModulesWithOneLoader(config, sysClassLoader);

創建圖層后,需要從該圖層中的模塊加載類。 模塊中的所有類型都由映射到該模塊的類加載器加載。 請注意,可能在多個層中定義了相同的模塊,但這些模塊將被映射到不同的類加載器。 ModuleLayer類包含一個findLoader(String moduleName)方法,它接受模塊名稱作為參數,並返回該模塊的類加載器。 如果模塊未在層中定義,則會檢查父層。 如果模塊不存在於此層或其祖先層中,則會拋出IllegalArgumentException異常。 一旦獲得了模塊的類加載器,可以調用它的`loadClass(String className)方法從該模塊加載一個類。 以下代碼片段(不包括異常處理邏輯)顯示了如何在圖層中加載類:

ModuleLayer layer = /* create a layer... */
// Load a class using the layer
String moduleName = "com.jdojo.layer";
String className = "com.jdojo.layer.LayerInfo";
Class<?> cls = layer.findLoader(moduleName)
                    .loadClass(className);

獲得Class對象后,可以使用它來實例化其對象並調用該對象的方法。 以下代碼段創建一個加載類的對象,並在該對象上調用printInfo的方法:

// A method name that prints the details of an object
String methodName = "printInfo";
// Instantiate the class using its no-args constructor
Object obj = cls.getConstructor().newInstance();
// Find the method
Method method = cls.getMethod(methodName);
// Call the method that will print the details
method.invoke(obj);

ModuleLayer類中的以下方法可用於獲取有關模塊層本身或模塊層中包含的模塊的信息:

Optional<Module> findModule(String moduleName)
Set<Module> modules()
List<ModuleLayer> parents()

findModule()方法在層或其父層中查找具有指定名稱的模塊。 modules()方法返回層中的一組模塊,如果該層不包含任何模塊,那么它可能是一個空集合。parent()方法返回此圖層的父層列表,如果是空層則為空。

接下來,介紹如何創建自定義層的完整示例,以及如何在同一應用程序中將兩個版本的同一模塊加載到兩個層中。

模塊名稱是com.jdojo.layer,它由一個名為com.jdojo.layer的包,它只包含一個名為LayerInfo的類。 有兩個版本的相同模塊,所以一切都將重復。 在源代碼中創建了兩個名為com.jdojo.layer.v1和com.jdojo.layer.v2的NetBeans項目。

下面包含com.jdojo.layer模塊的模塊定義的版本1.0

// module-info.com version 1.0
module com.jdojo.layer {
    exports com.jdojo.layer;
}

接下來是LayerInfo類的聲明。

// LayerInfo.java
package com.jdojo.layer;
public class LayerInfo {
    private final static String VERSION = "1.0";
    static {
        System.out.println("Loading LayerInfo version " + VERSION);
    }
    public void printInfo() {
        Class cls = this.getClass();
        ClassLoader loader = cls.getClassLoader();
        Module module = cls.getModule();
        String moduleName = module.getName();
        ModuleLayer layer = module.getLayer();
        System.out.println("Class Version: " + VERSION);
        System.out.println("Class Name: " + cls.getName());
        System.out.println("Class Loader: " + loader);
        System.out.println("Module Name: " + moduleName);
        System.out.println("Layer Name: " + layer);
    }
}

LayerInfo類非常簡單。 它將其版本信息保持在VERSION靜態變量中。 它在包含版本信息的靜態初始化程序中打印一條消息。 此消息將幫助你了解哪個版本的LayerInfo類正在加載。 printInfo()方法打印類的詳細信息:版本,類名,類加載器,模塊名稱和模塊層。

下面分別包含com.jdojo.layer模塊的模塊定義的2.0版本和LayerInfo類的類聲明。 只有一件事情從這個模塊的版本1.0改為版本2.0,靜態變量VERSION的值從1.0變為2.0。

// module-info.com version 2.0
module com.jdojo.layer {
    exports com.jdojo.layer;
}
// LayerInfo.java
package com.jdojo.layer;
public class LayerInfo {
    private final static String VERSION = "2.0";
    static {
        System.out.println("Loading LayerInfo version " + VERSION);
    }
    public void printInfo() {
        Class cls = this.getClass();
        ClassLoader loader = cls.getClassLoader();
        Module module = cls.getModule();
        String moduleName = module.getName();
        ModuleLayer layer = module.getLayer();
        System.out.println("Class Version: " + VERSION);
        System.out.println("Class Name: " + cls.getName());
        System.out.println("Class Loader: " + loader);
        System.out.println("Module Name: " + moduleName);
        System.out.println("Layer Name: " + layer);
    }
}

可以測試模塊層,並將com.jdojo.layer模塊的兩個版本都加載到同一個JVM中的兩個不同的層中。 為此模塊的版本2.0創建一個模塊化JAR,將其命名為com.jdojo.layer.v2.jar或給任何其他所需的名稱,並將模塊化JAR放入C:\Java9Revealed\customLib目錄中。

測試模塊層的程序在com.jdojo.layer.test模塊中,其聲明下所示。 該模塊聲明對com.jdojo.layer模塊的版本1.0的依賴。 如何確保com.jdojo.layer模塊的1.0版與com.jdojo.layer.test模塊一起使用? 所有需要做的是在運行com.jdojo.layer.test模塊時將com.jdojo.layer模塊的1.0版代碼放在模塊路徑上。 要在NetBeans中實現此目的,請將com.jdojo.layer.v1項目添加到com.jdojo.layer.test模塊的模塊路徑中。

// module-info.java
module com.jdojo.layer.test {
    // This module reads version 1.0 of the com.jdojo.layer module
    requires com.jdojo.layer;
}

下面包含了LayerTest類的代碼,它包含了創建自定義層並將模塊加載到其中的邏輯。 此類中使用的邏輯的詳細說明遵循此類的輸出。

// LayerTest.java
package com.jdojo.layer.test;
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;
public class LayerTest {
    public static void main(String[] args) {
        /* Location for the custom module. You will need to change the
           path to point to a directory on your PC that contains the
           modular JAR for the com.jdojo.layer (version 2.0) module.
         */
        final String CUSTOM_MODULE_LOCATION = "C:\\Java9Revealed\\customLib";
        // Define the set of root modules to be resolved in the custom layer
        Set<String> rootModules = Set.of("com.jdojo.layer");
        // Create a custom layer
        ModuleLayer customLayer = createLayer(CUSTOM_MODULE_LOCATION, rootModules);
        // Test the class in the boot layer
        ModuleLayer bootLayer = ModuleLayer.boot();
        testLayer(bootLayer);
        System.out.println();
        // Test the class in the custom layer
        testLayer(customLayer);
    }
    public static ModuleLayer createLayer(String modulePath, Set<String> rootModules) {
        Path path = Paths.get(modulePath);
        // Define the module finders to be used in creating a
        // configuration for the custom layer
        ModuleFinder beforFinder = ModuleFinder.of(path);
        ModuleFinder afterFinder = ModuleFinder.of();
        // Create a configuration for the custom layer
        Configuration parentConfig = ModuleLayer.boot().configuration();
        Configuration config =
                parentConfig.resolve(beforFinder, afterFinder, rootModules);
        /* Create a custom layer with one class loader. The parent for
           the class loader is the system class loader. The boot layer is
           the parent layer of this custom layer.
         */
        ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
        ModuleLayer parentLayer = ModuleLayer.boot();
        ModuleLayer layer = parentLayer.defineModulesWithOneLoader(config, sysClassLoader);
        // Check if we loaded the module in this layer
        if (layer.modules().isEmpty()) {
            System.out.println("\nCould not find the module " + rootModules
                    + " at " + modulePath + ". "
                    + "Please make sure that the com.jdojo.layer.v2.jar exists "
                    + "at this location." + "\n");
        }
        return layer;
    }
    public static void testLayer(ModuleLayer layer) {
        final String moduleName = "com.jdojo.layer";
        final String className = "com.jdojo.layer.LayerInfo";
        final String methodName = "printInfo";
        try {
            // Load the class
            Class<?> cls = layer.findLoader(moduleName)
                                .loadClass(className);
            // Instantiate the class using its no-args constructor
            Object obj = cls.getConstructor().newInstance();
            // Find the method
            Method method = cls.getMethod(methodName);
            // Call the method that will print the details
            method.invoke(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

main()方法聲明CUSTOM_MODULE_LOCATION的變量,它b保存com.jdojo.layer模塊2.0版本的位置。 必須將路徑更改為指向計算機上包含com.jdojo.layer模塊版本2.0的編譯模塊代碼的目錄。

final String CUSTOM_MODULE_LOCATION = "C:\\Java9Revealed\\customLib";

下面代碼保存com.jojo.layer作為自定義圖層配置的唯一根模塊:

Set<String> rootModules = Set.of("com.jdojo.layer");

調用createLayer()方法來創建自定義模塊層。 該方法使用邏輯在CUSTOM_MODULE_LOCATION創建com.jdojo.layer模塊版本2.0的自定義層:

ModuleLayer customLayer = createLayer(CUSTOM_MODULE_LOCATION, rootModules);

main()方法獲取引導層的引用:

ModuleLayer bootLayer = ModuleLayer.boot();

現在,testLayer()方法被調用一次用於引導層,一次用於自定義層。 該方法在模塊層中找到com.jdojo.layer模塊的類加載器,並加載com.jdojo.layer.LayerInfo類。

final String moduleName = "com.jdojo.layer";
final String className = "com.jdojo.layer.LayerInfo";
final String methodName = "printInfo";
Class<?> cls = layer.findLoader(moduleName)
                    .loadClass(className);

使用無參構造方法創建LayerInfo對象。

Object obj = cls.getConstructor().newInstance();

最后,獲取了LayerInfo類的printInfo()方法的引用,並調用了printInfo()方法,該方法打印了LayerInfo類的詳細信息:

Method method = cls.getMethod(methodName);
method.invoke(obj);

可以在NetBeans中運行LayerTest類,也可以使用以下命令。 可能會得到不同的輸出。 層名稱是該層中所有模塊的列表,由ModuleLayer類的toString()方法返回。

C:\Java9Revealed>java --module-path com.jdojo.layer.v1\dist;com.jdojo.layer.test\dist
--module com.jdojo.layer.test/com.jdojo.layer.test.LayerTest

輸出結果為:

Loading LayerInfo version 1.0
Class Version: 1.0
Class Name: com.jdojo.layer.LayerInfo
Class Loader: jdk.internal.loader.ClassLoaders$AppClassLoader@6e3c1e69
Module Name: com.jdojo.layer
Layer Name: java.security.jgss, jdk.unsupported, jdk.jlink, jdk.security.jgss, jdk.javadoc, jdk.crypto.cryptoki, java.naming, jdk.jartool, java.xml.crypto, jdk.deploy, java.logging, jdk.snmp, jdk.zipfs, jdk.crypto.mscapi, jdk.naming.dns, java.smartcardio, java.base, jdk.crypto.ec, jdk.dynalink, jdk.compiler, java.compiler, jdk.jdeps, java.rmi, java.xml, com.jdojo.layer.test, jdk.management, java.datatransfer, jdk.scripting.nashorn, java.desktop, java.management, jdk.naming.rmi, java.scripting, jdk.localedata, jdk.accessibility, jdk.charsets, com.jdojo.layer, java.security.sasl, jdk.security.auth, jdk.internal.opt, java.prefs
Loading LayerInfo version 2.0
Class Version: 2.0
Class Name: com.jdojo.layer.LayerInfo
Class Loader: jdk.internal.loader.Loader@4cb2c100
Module Name: com.jdojo.layer
Layer Name: com.jdojo.layer

十五. 總結

模塊API由類和接口組成,可以編程的方式訪問模塊。 使用API,可以以編程方式讀取/修改/構建模塊描述,加載模塊,讀取模塊的內容,創建模塊層等。模塊API很小,包含大約15個類和接口,分布在兩個包之間:java.lang和java.lang.module。 ModuleModuleLayerLayerInstantiationException類在java.lang包中,其余的在java.lang.module包中。

Module類的實例代表運行時模塊。 加載到JVM中的每個類型都屬於一個模塊。 JDK 9將getModule()的方法添加到Class類中,該類返回該類所屬的模塊。

ModuleDescriptor類的實例表示一個模塊定義,它是從模塊聲明創建的——通常來自一個module-info.class文件。模塊描述也可以使用ModuleDescriptor.Builder類即時創建。可以使用命令行選項來擴充模塊聲明,例如--add-reads--add-exports-add-opens,並使用Module類中的方法,如addReads()addOpens()addExports()ModuleDescriptor表示在模塊聲明時存在的模塊描述,而不是增強的模塊描述。 Module類的getDescriptor()方法返回ModuleDescriptorModuleDescriptor是不可變類的。未命名的模塊沒有模塊描述。 Module類的getDescriptor()方法為未命名的模塊返回null。 ModuleDescriptor類包含幾個嵌套類,例如ModuleDescriptor.Requires嵌套類;它們每個代表程序中的一個模塊語句。

可以使用命令行選項擴充模塊描述,並以編程方式使用Module API。 可以將模塊屬性的所有查詢分為兩類:在加載模塊后可能會更改的模塊的查詢和在模塊加載后不更改的模塊的屬性。Module類包含第一類中查詢的方法,ModuleDescriptor類包含第二類中查詢的方法。
可以使用Module類中的addExports()addOpens()addReads()addUses()方法在運行時更新模塊的定義。

可以使用模塊聲明上的注解。 java.lang.annotation.ElementType枚舉有MODULE的新值。可以在注解聲明上使用MODULE作為目標類型,允許在模塊上使用注解類型。在Java 9中,兩個注解java.lang.Deprecatedjava.lang.SuppressWarnings已更新為在模塊聲明中使用。在模塊上使用這些注解只影響模塊聲明,而不影響模塊中包含的類型。

模塊安排成層。一個模塊層是一組解析的模塊,具有將每個模塊映射到負責加載該模塊中所有類型的類加載器的功能。解析模塊的集合稱為配置。層次分層排列。層除了空層以外還有至少一個父層,顧名思義,它不含任何模塊,主要用作引導層的父層。引導層由啟動時由JVM創建,通過針對一組可觀察模塊解析應用程序的初始模塊(根模塊)。可以創建自定義圖層。模塊層允許將同一模塊的多個版本加載到不同的層中,並在同一個JVM中使用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM