Tips

做一个终身学习的人。

在本章节中,主要介绍以下内容:

  • 什么是模块 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中使用。

Java 9 揭秘(10. 模块API)的更多相关文章

  1. Java 9 揭秘全目录汇总

    Tips 做一个终身学习的人. 当写这篇文章时,关于Java 9的学习就先告一段落了. 首先介绍一下背景,大概两个月前,我突然有兴趣想看看Java 9,当时读了一本英文原著<Java 9 Rev ...

  2. 《Java 9 揭秘》全目录汇总

    Tips 做一个终身学习的人. 当写这篇文章时,关于Java 9的学习就先告一段落了. 首先介绍一下背景,大概两个月前,我突然有兴趣想看看Java 9,当时读了一本英文原著<Java 9 Rev ...

  3. Java 9 揭秘(4. 模块依赖)

    文 by / 林本托 Tips 做一个终身学习的人. 在此章节中,主要学习以下内容: 如何声明模块依赖 模块的隐式可读性意味着什么以及如何声明它 限定导出(exports)与非限定导出之间的差异 声明 ...

  4. Java 9 揭秘(6. 封装模块)

    Tips 做一个终身学习的人. 在这章节中, 主要介绍以下内容: 封装Java模块的不同格式 JAR格式增强 什么是多版本JAR 如何创建和使用多版本JAR JMOD是什么格式 如何使用jmod工具来 ...

  5. Java 9 揭秘(12. Process API 更新)

    Tips 做一个终身学习的人. 在本章中,主要介绍以下内容: Process API是什么 如何创建本地进程 如何获取新进程的信息 如何获取当前进程的信息 如何获取所有系统进程的信息 如何设置创建,查 ...

  6. Java 9 揭秘(20. JDK 9中API层次的改变)

    Tips 做一个终身学习的人. 在最后一章内容中,主要介绍以下内容: 下划线作为新关键字 改进使用try-with-resources块的语法 如何在匿名类中使用<>操作符 如何在接口中使 ...

  7. Java 9 揭秘(3. 创建你的第一个模块)

    文 by / 林本托 Tips 做一个终身学习的人. 在这个章节中,主要介绍以下内容: 如何编写模块化的Java程序 如何编译模块化程序 如何将模块的项目打包成模块化的JAR文件 如何运行模块化程序 ...

  8. Java 9 揭秘(1.Java入门介绍)

    文 by / 林本托 在第一部分中,主要讲解如下内容: JDK 9 包含了哪些内容 运行代码的系统要求 如何安装 NetBeans 1 JDK 介绍 JDK 9是Java开发工具包的第九个主要版本,计 ...

  9. Java 9 揭秘(11. Java Shell)

    Tips 做一个终身学习的人. 在本章节中,主要介绍以下内容: 什么是Java shell JShell工具和JShell API是什么 如何配置JShell工具 如何使用JShell工具对Java代 ...

随机推荐

  1. 线程机制、CLR线程池以及应用程序域

    最近在总结多线程.CLR线程池以及TPL编程实践,重读一遍CLR via C#,比刚上班的时候收获还是很大的.还得要多读书,读好书,同时要多总结,多实践,把技术研究透,使用好. 话不多说,直接上博文吧 ...

  2. 抱歉,您必须拥有一个终端来执行 sudo

    Linux ssh执行远端服务器sudo命令时有如下报错: sudo: sorry, you must have a tty to run sudo sudo:抱歉,您必须拥有一个终端来执行 sudo ...

  3. R语言机器学习之caret包运用

    在大数据如火如荼的时候,机器学习无疑成为了炙手可热的工具,机器学习是计算机科学和统计学的交叉学科, 旨在通过收集和分析数据的基础上,建立一系列的算法,模型对实际问题进行预测或分类. R语言无疑为我们提 ...

  4. python基础 - 01

    python 变量名 在python中的变量命名,与其他语言大体相似,变量的命名规则如下: 变量名是数字.字母.下划线的任意组合 变量名的第一个字符不能是数字 系统的关键字不能设置为变量名    Ti ...

  5. Eclipse之文件【默认编码格式设置】,防止乱码等问题

    文件默认编码格式设置步骤如下: 这里显示的是workspace的视图 其他格式文件的视图如下:

  6. java中的流程控制语句总结

    程序的结构分类: 顺序结构:按照写代码的顺序 一次执行 选择结构:根据条件的不同有选择的执行不同的代码 循环结构:在一定条件下 反复执行某一片代码 选择结构: 也叫分支结构 根据条件的不同,有选择的执 ...

  7. CSS3学习系列之选择器(二)

    first-child选择器和last-child选择器 first-child指定第一个元素.last-child指定最后一个子元素. 例如: <!DOCTYPE html> <h ...

  8. 关于Atlassian无法注册的问题,请看过来

    好多童鞋在用团队构建工具git的时候,必然用到git的可视化工具sourceTree来管理项目一些操作,那么当我们下载完sourTree的时候,会有一个选择,已有账户登录还是免费账户,免费账户只有三十 ...

  9. Linux文件锁定保护命令chattr介绍

    chattr命令的用法:chattr [ -RV ] [ -v version ] [ mode ] files- 最关键的是在[mode]部分,[mode]部分是由+-=和[ASacDdIijsTt ...

  10. 关于MATLAB处理大数据坐标文件2017528

    第一次提交数据 增加了部分特征 3000数据测试中得分99 但是10万数据出现过拟化现象,正确率下降 总结:1.某些特征数据本身波动不大应该考虑放弃 2.一些特征虽然表面觉得差异显而易见,但是数据表达 ...