1. 概述
继《Java 9 模块化指南》之后,本文将深入探讨随 Java 平台模块系统(JPMS)引入的 java.lang.Module
API。
这个 API 提供了在运行时以编程方式访问模块的能力,可以获取模块的详细信息,比如名称、依赖、导出包等,并对其进行动态操作。核心是通过 Module
和其关联的 ModuleDescriptor
来实现对模块元数据的读取与修改。
2. 读取模块信息
Module
类代表了命名模块(named module)和未命名模块(unnamed module)。
- ✅ 命名模块:拥有明确名称,由 JVM 在构建模块层(Module Layer)时,根据模块图(module graph)创建。
- ✅ 未命名模块:没有名称,每个
ClassLoader
都对应一个未命名模块。所有不属于任何命名模块的类,都属于其类加载器关联的未命名模块。
Module
的价值在于它提供了一系列方法,让我们能动态获取模块的元信息,比如模块名、类加载器、包含的包等。
2.1 判断命名模块 vs 未命名模块
通过 isNamed()
方法可以轻松判断一个类所属的模块类型。
以 HashMap
为例,它属于 Java 核心模块:
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
}
检查其模块信息:
Module module = Person.class.getModule();
assertThat(module.isNamed(), is(false));
assertThat(module.getName(), is(nullValue()));
⚠️ 踩坑提醒:在传统的 classpath 下编译的类,即使你用了 module-info.java
,如果没正确打包成模块 JAR,它们依然会落在未命名模块里,isNamed()
返回 false
。
2.2 获取模块中的包
想知道某个模块包含了哪些包?用 getPackages()
方法:
assertTrue(javaBaseModule.getPackages().contains("java.lang.annotation"));
assertFalse(javaBaseModule.getPackages().contains("java.sql"));
这个集合包含了该模块声明导出(exports)和自动导出的所有包。
2.3 获取模块注解
模块也可以有注解,比如 @Deprecated
。使用 getAnnotations()
可以获取:
assertThat(javaBaseModule.getAnnotations().length, is(0));
- ✅ 命名模块:如果没有注解,返回空数组。
- ✅ 未命名模块:调用此方法也返回空数组。
目前标准模块很少使用注解,但自定义模块可以利用这一点做标记。
2.4 获取类加载器
通过 getClassLoader()
可以拿到加载该模块的 ClassLoader
:
assertThat(
module.getClassLoader().getClass().getName(),
is("jdk.internal.loader.ClassLoaders$AppClassLoader")
);
这在调试类加载问题时非常有用,能清晰看到模块与类加载器的对应关系。
2.5 获取模块层(ModuleLayer)
ModuleLayer
是模块系统的核心概念之一,它代表了 JVM 中模块的一层视图。
- ✅ 模块层告诉 JVM 哪些类可以从哪些模块加载,从而精确控制类的归属。
- ✅ 它包含配置信息、父层引用以及该层中所有模块的集合。
获取模块所属的层:
ModuleLayer javaBaseModuleLayer = javaBaseModule.getLayer();
进一步查询层的信息:
assertTrue(javaBaseModuleLayer.configuration().findModule("java.base").isPresent());
⚠️ 特别说明:JVM 启动时会创建一个特殊的 boot layer(启动层),它是唯一包含 java.base
模块的层,其他层通常以它为父层。
3. 深入 ModuleDescriptor
ModuleDescriptor
是模块的“身份证”,它描述了一个命名模块的静态元数据,包括依赖、导出、服务提供等。它是不可变对象,线程安全。
3.1 获取 ModuleDescriptor
最直接的方式是从 Module
对象获取:
ModuleDescriptor moduleDescriptor = javaBaseModule.getDescriptor();
3.2 创建 ModuleDescriptor
除了读取,你还可以通过 ModuleDescriptor.Builder
动态创建一个描述符:
ModuleDescriptor.Builder moduleBuilder = ModuleDescriptor
.newModule("baeldung.base");
ModuleDescriptor moduleDescriptor = moduleBuilder.build();
assertThat(moduleDescriptor.name(), is("baeldung.base"));
- ✅
newModule()
:创建普通模块。 - ✅
newOpenModule()
:创建开放模块(open module),所有包对反射开放。 - ✅
newAutomaticModule()
:创建自动模块(automatic module),用于兼容老的 JAR 包。
3.3 判断模块类型
通过 ModuleDescriptor
可以判断模块是普通、开放还是自动模块:
ModuleDescriptor moduleDescriptor = javaBaseModule.getDescriptor();
assertFalse(moduleDescriptor.isAutomatic());
assertFalse(moduleDescriptor.isOpen());
3.4 获取依赖(requires)
requires()
方法返回模块的依赖列表(Set<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 获取服务提供(provides)
provides()
返回模块提供的服务实现:
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());
例如 java.base
提供了文件系统 SPI 的实现。
3.6 获取导出包(exports)
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"));
⚠️ 自动模块不会显式导出包,其 exports()
返回空集。
3.7 获取服务使用(uses)
uses()
返回模块所依赖的服务接口:
Set<String> javaSqlUses = javaSqlModule.getDescriptor().uses();
assertThat(javaSqlUses, hasItem("java.sql.Driver"));
这对应 module-info.java
中的 uses java.sql.Driver;
语句。
⚠️ 自动模块的 uses()
集合为空。
3.8 获取开放包(opens)
opens()
返回模块通过 opens
关键字开放给反射的包:
Set<Opens> javaBaseUses = javaBaseModule.getDescriptor().opens();
Set<Opens> javaSqlUses = javaSqlModule.getDescriptor().opens();
assertThat(javaBaseUses, empty());
assertThat(javaSqlUses, empty());
- ✅ 普通模块:只有显式
opens
的包才会出现在这里。 - ✅ 开放模块(open module):所有包都对反射开放,但
opens()
返回空,因为它是模块级别的开放。
4. 动态操作模块
除了读取,Module
API 还允许在运行时动态修改模块的可访问性,这在框架或测试中非常有用。
4.1 添加导出(addExports)
让当前模块将某个包导出给另一个模块:
Module updatedModule = module.addExports(
"com.baeldung.java9.modules", javaSqlModule);
assertTrue(updatedModule.isExported("com.baeldung.java9.modules"));
- ✅ 仅当调用者的模块是代码所属模块时才有效。
- ⚠️ 如果包已导出或目标模块是开放模块,则无效果。
4.2 添加读取权限(addReads)
让当前模块读取另一个模块:
Module updatedModule = module.addReads(javaSqlModule);
assertTrue(updatedModule.canRead(javaSqlModule));
- ✅ 模块默认可以读自己,所以给自己加读取无效。
- ⚠️ 未命名模块或已存在读取关系时,调用无效果。
4.3 添加开放包(addOpens)
将某个包对另一个模块开放(用于反射):
Module updatedModule = module.addOpens(
"com.baeldung.java9.modules", javaSqlModule);
assertTrue(updatedModule.isOpen("com.baeldung.java9.modules", javaSqlModule));
- ✅ 类似
--add-opens
JVM 参数的运行时版本。 - ⚠️ 如果包已对目标模块开放,则无效果。
4.4 添加服务依赖(addUses)
动态声明模块使用某个服务:
Module updatedModule = module.addUses(Driver.class);
assertTrue(updatedModule.canUse(Driver.class));
- ⚠️ 对未命名模块或自动模块调用此方法无效。
5. 总结
本文系统性地介绍了 java.lang.Module
API 的核心能力:
- ✅ 读取模块元信息(名称、包、类加载器、层)。
- ✅ 通过
ModuleDescriptor
解析模块的依赖、导出、服务等结构。 - ✅ 在运行时动态修改模块的可访问性(导出、读取、开放、使用)。
这套 API 为构建灵活的模块化框架和解决模块化环境下的反射、类加载问题提供了强大支持。
所有示例代码均可在 GitHub 上获取:https://github.com/baeldung/tutorials/tree/master/core-java-modules/core-java-9-jigsaw