天天看點

Java 9 揭秘(10. 子產品API)

Tips

做一個終身學習的人。

Java 9 揭秘(10. 子產品API)

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

  • 什麼是子產品 API
  • 如何在程式中表示子產品和子產品描述
  • 如何讀取程式中的子產品描述
  • 如何表示子產品的版本
  • 如何使用

    Module

    ModuleDescriptor

    類讀取子產品的屬性
  • Module

    類在運作時更新子產品的定義
  • 如何建立可用于子產品的注解以及如何讀取子產品上使用的注解
  • 什麼是子產品層和配置
  • 如何建立自定義子產品層并将子產品加載到它們中

一. 什麼是子產品API

子產品API由可以讓你對子產品進行程式設計通路的類和接口組成。 使用API,可以通過程式設計方式:

  • 讀取,修改和構模組化塊描述符
  • 加載子產品
  • 讀取子產品的内容
  • 搜尋加載的子產品
  • 建立新的子產品層

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

  • java.lang
  • java.lang.module

Module

ModuleLayer

LayerInstantiationException

類在java.lang包中,其餘的在java.lang.module包中。 下表包含子產品API中的類的清單,每個類的簡要說明。 清單未排序。 首先列出了

Module

ModuleDescriptor

,因為應用程式開發人員最常使用它們。 所有其他類通常由容器和類庫使用。 該清單不包含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

類是從類路徑加載的,

m1

m2

指的是同一個子產品:

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();
           

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.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

類型,導出該包,對于非限定的導出,它傳回一個空的

set

modifiers()

方法傳回一系列

exports

語句的修飾符,它們是

ModuleDescriptor.Exports.Modifier

枚舉的常量,它包含以下兩個常量:

  • MANDATED:源子產品聲明中的

    exports

    隐式聲明。
  • SYNTHETIC:源子產品聲明中的

    exports

    未明确或隐含地聲明。

六. 表示

opens

ModuleDescriptor.Opens

類的執行個體表示子產品聲明中的一個

opens

語句。 類中的以下方法傳回了

opens

語句的元件:

  • Set<ModuleDescriptor.Opens.Modifier> modifiers()

isCualified()

方法對于限定的打開傳回true,對于非限定打開,傳回false。

source()

方法傳回打開包的名稱。 對于限定的打開,

targets()

set

類型,打開該包,對于非限定打開,它傳回一個空

set

。 該

modifiers()

方法傳回一系列的

opens

語句,它們是嵌套的

ModuleDescriptor.Opens.Modifier

  • MANDATED:源子產品聲明的中的

    opens

  • 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.B

Y.Z

service()

方法傳回服務類型的全限定名稱。 在前面的例子中,它将傳回

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>

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()
  • 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.B

X.Y1

X.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

的類的代碼,它使用

Module

ModuleDescriptor

類列印三個子產品的子產品詳細資訊。

// 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]
           

第二次運作,

ModuleBasicInfo

PrimeChecker

類被加載到應用程式類加載器的未命名子產品中,這反映在為兩個子產品

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.Deprecated

java.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

作為目标。

不能對各個子產品語句添加注解。 例如,不能使用@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.Thread

com.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)

方法來加載服務提供者。 指定的層将是托管的應用程式特定層。 此方法從指定的層及其父層加載服務提供者。

層是不可變的。 建立圖層後,無法向其中添加子產品或從中删除子產品。 如果需要添加子產品或替換其他版本的子產品,則必須拆除圖層并重新建立。

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

  • 建立子產品查找器
  • 建立一組根子產品
  • 建立配置對象
  • 建立一個圖層

建立圖層後,可以使用它來加載類型。 将在下一節詳細介紹這些步驟。 最後,展示多個版本的子產品如何使用圖層。

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)

方法來釋放位元組緩沖區,以避免資源洩漏。

下列包含一個程式,顯示如何讀取子產品的内容。 它讀取

ByteBuffer

Object

對象的内容,并以位元組為機關列印其大小。 它還在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

語句子產品的額外可讀性,進而産生一個稱為可讀性圖的子產品圖。

子產品中的

uses

provides

語句也構成依賴關系。 如果子產品M使用服務類型S,并且另一個子產品N提供T的實作S,則子產品M依賴于使用服務類型S的子產品N。可讀性圖用針對這樣的服務使用依賴性計算的子產品進行擴充。

當建立引導層的配置時,它通過解析依賴關系(

requires

語句),隐含的可讀性(

requires transitive

)和服務使用依賴性(

uses

provides

語句)來包含子產品。 為使用者定義的層建立配置時,可以選擇包含或排除服務使用依賴關系。

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。

Module

ModuleLayer

LayerInstantiationException

類在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()

ModuleDescriptor

ModuleDescriptor

是不可變類的。未命名的子產品沒有子產品描述。

Module

getDescriptor()

ModuleDescriptor

類包含幾個嵌套類,例如

ModuleDescriptor.Requires

嵌套類;它們每個代表程式中的一個子產品語句。

可以使用指令行選項擴充子產品描述,并以程式設計方式使用Module API。 可以将子產品屬性的所有查詢分為兩類:在加載子產品後可能會更改的子產品的查詢和在子產品加載後不更改的子產品的屬性。

Module

ModuleDescriptor

Module

addExports()

addOpens()

addReads()

addUses()

方法在運作時更新子產品的定義。

可以使用子產品聲明上的注解。

java.lang.annotation.ElementType

枚舉有

MODULE

的新值。可以在注解聲明上使用

MODULE

作為目标類型,允許在子產品上使用注解類型。在Java 9中,兩個注解

java.lang.Deprecated

java.lang.SuppressWarnings

已更新為在子產品聲明中使用。在子產品上使用這些注解隻影響子產品聲明,而不影響子產品中包含的類型。

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