Java 9 揭秘(10. 模块API)
Tips
做一个终身学习的人。

在本章节中,主要介绍以下内容:
- 什么是模块 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();
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<String> targets()
isCualified()
方法对于限定的导出返回true,对于非限定的导出,返回false。 source()
方法返回导出的包的名称。 对于限定的导出,targets()
方法返回一个不可变的模块名称set
类型,导出该包,对于非限定的导出,它返回一个空的set
。 modifiers()
方法返回一系列exports
语句的修饰符,它们是ModuleDescriptor.Exports.Modifier
枚举的常量,它包含以下两个常量:
- MANDATED:源模块声明中的
exports
隐式声明。 - SYNTHETIC:源模块声明中的
exports
未明确或隐含地声明。
六. 表示opens
语句
ModuleDescriptor.Opens
类的实例表示模块声明中的一个opens
语句。 类中的以下方法返回了opens
语句的组件:
- boolean isQualified()
- Set<ModuleDescriptor.Opens.Modifier> modifiers()
- String source()
- Set<String> 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<String> providers()
- String service()
providers()
方法返回提供者类的完全限定类名的列表。 在上一个示例中,返回的列表将包含A.B
和Y.Z
。service()
方法返回服务类型的全限定名称。 在前面的例子中,它将返回X.Y
.
八. 表示requires
语句
ModuleDescriptor.Requires
类的实例表示模块声明中的requires
语句。 类中的以下方法返回requires
语句的组件:
- Optional<ModuleDescriptor.Version> compiledVersion()
- Optional<String> 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<String> mainClass()
- String name()
- Set<ModuleDescriptor.Opens> opens()
- Set<String> packages()
- Set<ModuleDescriptor.Provides> provides()
- Optional<String> rawVersion()
- Set<ModuleDescriptor.Requires> requires()
- String toNameAndVersion()
- Set<String> 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
作为目标。
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.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