Contents

Java 9 中Module API简介

1. 简介

遵循Java 9 模块化指南 ,在本文中,我们将探索与 Java 平台模块系统一起引入的java.lang.Module API。 此 API 提供了一种以编程方式访问模块、从模块中检索特定信息以及通常使用它及其Module Descriptor的方法。

2. 读取模块信息

Module类代表命名和未命名的模块。命名模块有一个名称,由 Java 虚拟机在创建模块层时使用模块图作为定义来构建。

未命名的模块没有名称,每个ClassLoader 都有一个名称。 不在命名模块中的所有类型都是与其类加载器相关的未命名模块的成员。

Module类的有趣部分在于它公开了允许我们从模块中检索信息的方法,例如模块名称、模块类加载器和模块中的包。

让我们看看如何找出一个模块是命名的还是未命名的。

2.1. 命名或未命名

使用*isNamed()*方法,我们可以识别模块是否被命名。

让我们看看如何查看给定类(如HashMap)是否是命名模块的一部分,以及如何检索其名称:

Module javaBaseModule = HashMap.class.getModule();
assertThat(javaBaseModule.isNamed(), is(true));
assertThat(javaBaseModule.getName(), is("java.base"));

现在让我们定义一个Person类:

public class Person {
    private String name;
    // constructor, getters and setters
}

与我们对HashMap类所做的一样,我们可以检查Person类是否是命名模块的一部分:

Module module = Person.class.getModule();
assertThat(module.isNamed(), is(false));
assertThat(module.getName(), is(nullValue()));

2.2. 套餐

使用模块时,了解模块中可用的包可能很重要。 让我们看看如何检查给定的包,例如java.lang.annotation是否包含在给定的模块中:

assertTrue(javaBaseModule.getPackages().contains("java.lang.annotation"));
assertFalse(javaBaseModule.getPackages().contains("java.sql"));

2.3. 注解

同样,对于包,可以使用getAnnotations()方法检索模块中存在的注解。 如果命名模块中不存在注释,则该方法将返回一个空数组。 让我们看看java.base模块中有多少注解:

assertThat(javaBaseModule.getAnnotations().length, is(0));

在未命名的模块上调用时,*getAnnotations()*方法将返回一个空数组。

2.4. 类加载器

感谢Module类中可用的**getClassLoader()方法,我们可以检索给定模块的ClassLoader

assertThat(
  module.getClassLoader().getClass().getName(), 
  is("jdk.internal.loader.ClassLoaders$AppClassLoader")
);

2.5. 层

可以从模块中提取的另一个有价值的信息是ModuleLayer,它表示 Java 虚拟机中的一层模块。

模块层通知 JVM 可以从模块加载的类。通过这种方式,JVM 准确地知道每个类是哪个模块的成员。

ModuleLayer包含与其配置、父层和层内可用模块集相关的信息。

让我们看看如何检索给定模块的ModuleLayer

ModuleLayer javaBaseModuleLayer = javaBaseModule.getLayer();

一旦我们检索到ModuleLayer,我们就可以访问它的信息:

assertTrue(javaBaseModuleLayer.configuration().findModule("java.base").isPresent());

一个特例是启动层,它是在 Java 虚拟机启动时创建的。引导层是唯一包含java.base模块的层。

3. 处理ModuleDescriptor

ModuleDescriptor描述了一个命名模块并定义了获取其每个组件的方法。

ModuleDescriptor对象是不可变且安全的,可供多个并发线程使用。 让我们从如何检索ModuleDescriptor开始。

3.1. 检索ModuleDescriptor

由于ModuleDescriptorModule紧密相连,因此可以直接从Module 中检索它:

ModuleDescriptor moduleDescriptor = javaBaseModule.getDescriptor();

3.2. 创建ModuleDescriptor

也可以使用ModuleDescriptor.Builder或通过读取模块声明的二进制形式module-info.class创建模块描述符。 让我们看看如何使用ModuleDescriptor.Builder API 创建模块描述符:

ModuleDescriptor.Builder moduleBuilder = ModuleDescriptor
  .newModule("blogdemo.base");
ModuleDescriptor moduleDescriptor = moduleBuilder.build();
assertThat(moduleDescriptor.name(), is("blogdemo.base"));

有了这个,我们创建了一个普通模块,但如果我们想创建一个开放模块或自动模块,我们可以分别使用*newOpenModule()newAutomaticModule()*方法。

3.3. 对模块进行分类

一个模块描述符描述一个普通的、开放的或自动的模块。

由于ModuleDescriptor中可用的方法,可以识别模块的类型:

ModuleDescriptor moduleDescriptor = javaBaseModule.getDescriptor();
assertFalse(moduleDescriptor.isAutomatic());
assertFalse(moduleDescriptor.isOpen());

3.4. 检索需求

使用模块描述符,可以检索代表模块依赖关系的Requires集。

这可以使用*requires()*方法:

Set<Requires> javaBaseRequires = javaBaseModule.getDescriptor().requires();
Set<Requires> javaSqlRequires = javaSqlModule.getDescriptor().requires();
Set<String> javaSqlRequiresNames = javaSqlRequires.stream()
  .map(Requires::name)
  .collect(Collectors.toSet());
assertThat(javaBaseRequires, empty());
assertThat(javaSqlRequiresNames, hasItems("java.base", "java.xml", "java.logging"));

所有模块,除了java.base,有java.base模块作为依赖项

但是,如果模块是自动模块,则依赖项集将为空,但java.base除外。

3.5. 检索提供

使用*provide()*方法可以检索模块提供的服务列表:

Set<Provides> javaBaseProvides = javaBaseModule.getDescriptor().provides();
Set<Provides> javaSqlProvides = javaSqlModule.getDescriptor().provides();
Set<String> javaBaseProvidesService = javaBaseProvides.stream()
  .map(Provides::service)
  .collect(Collectors.toSet());
assertThat(javaBaseProvidesService, hasItem("java.nio.file.spi.FileSystemProvider"));
assertThat(javaSqlProvides, empty());

3.6. 检索导出

使用*exports()*方法,我们可以找出模块是否导出包,特别是哪些:

Set<Exports> javaSqlExports = javaSqlModule.getDescriptor().exports();
Set<String> javaSqlExportsSource = javaSqlExports.stream()
  .map(Exports::source)
  .collect(Collectors.toSet());
assertThat(javaSqlExportsSource, hasItems("java.sql", "javax.sql"));

作为一种特殊情况,如果模块是自动模块,则导出的包集将为空。

3.7. 检索用途

使用*uses()*方法,可以检索模块的一组服务依赖项:

Set<String> javaSqlUses = javaSqlModule.getDescriptor().uses();
assertThat(javaSqlUses, hasItem("java.sql.Driver"));

如果模块是自动模块,则依赖项集将为空。

3.8. 检索打开

每当我们想要检索模块的打开包列表时,我们可以使用*opens()*方法:

Set<Opens> javaBaseUses = javaBaseModule.getDescriptor().opens();
Set<Opens> javaSqlUses = javaSqlModule.getDescriptor().opens();
assertThat(javaBaseUses, empty());
assertThat(javaSqlUses, empty());

如果模块是打开的或自动的,则该集合将为空。

4. 处理模块

使用ModuleAPI,除了从模块中读取信息之外,我们还可以更新模块定义。

4.1. 添加导出

让我们看看如何更新模块,从给定模块导出给定包:

Module updatedModule = module.addExports(
  "com.blogdemo.java9.modules", javaSqlModule);
assertTrue(updatedModule.isExported("com.blogdemo.java9.modules"));

仅当调用者的模块是代码所属的模块时才可以这样做。 附带说明一下,如果包已经由模块导出或者模块是打开的,则没有任何影响。

4.2. 添加读取

当我们想要更新模块以读取给定模块时,我们可以使用*addReads()*方法:

Module updatedModule = module.addReads(javaSqlModule);
assertTrue(updatedModule.canRead(javaSqlModule));

如果我们添加模块本身,则此方法不会执行任何操作,因为所有模块都会自行读取。 同样,如果模块是一个未命名的模块或者这个模块已经读取了另一个模块,这个方法什么也不做。

4.3. 添加打开

当我们想将一个已经打开包的模块更新到至少调用者模块时,我们可以使用*addOpens()*将包打开到另一个模块:

Module updatedModule = module.addOpens(
  "com.blogdemo.java9.modules", javaSqlModule);
assertTrue(updatedModule.isOpen("com.blogdemo.java9.modules", javaSqlModule));

如果包已经对给定模块打开,则此方法无效。

4.4. 添加用途

每当我们想要更新一个添加服务依赖项的模块时,方法*addUses()*是我们的选择:

Module updatedModule = module.addUses(Driver.class);
assertTrue(updatedModule.canUse(Driver.class));

在未命名的模块或自动模块上调用此方法时不执行任何操作。