2. 模块系统与反射
Java 9 引入的模块系统彻底改变了反射的运作方式。在 Java 9 之前,反射 API 几乎无所不能,可以随意访问非公有类成员。但模块系统出现后,这种"超能力"受到了合理限制。下面我们深入分析两者的关系。
2.1. 底层模型
Java 模块系统的核心目标是实现强封装,这主要包含两个维度:
- 可读性(Readability):粗粒度概念,决定模块间是否存在依赖关系
- 可访问性(Accessibility):细粒度概念,控制类能否访问其他类的字段或方法,受类边界、包边界和模块边界三重约束
这两者的关系是:可读性优先,可访问性建立在可读性之上。例如:
- 如果类是
public
但未导出,可读性会阻止访问- 如果非公有类在已导出包中,可读性允许传递,但可访问性会拒绝访问
要增加可读性,可通过以下方式:
- 在模块声明中使用
requires
指令 - 命令行指定
--add-reads
选项 - 调用
Module.addReads
方法
要突破封装边界,可通过:
- 在模块声明中使用
opens
指令 - 命令行指定
--add-opens
选项 - 调用
Module.addOpens
方法
关键点:反射也无法绕过可读性和可访问性规则,否则会触发错误或警告。但要注意:使用反射时,运行时会自动在两个模块间建立可读性关系。这意味着如果出错,问题通常出在可访问性上。
2.2. 不同反射使用场景
Java 模块系统包含多种模块类型(命名模块、未命名模块、平台模块等),它们之间的反射访问规则各不相同。我们重点分析三种典型的非法反射访问场景:
命名模块 → 命名模块
- 使用深度反射(调用
setAccessible(true)
访问非公有成员) - 结果:抛出
IllegalAccessException
或InaccessibleObjectException
- 使用深度反射(调用
未命名模块 → 应用命名模块
- 同样使用深度反射
- 结果:同样抛出上述异常
未命名模块 → 平台模块
- 使用深度反射访问 JDK 内部 API
- 结果:抛出
IllegalAccessException
或显示警告信息
平台模块的警告信息格式:
WARNING: Illegal reflective access by $PERPETRATOR to $VICTIM
其中
$PERPETRATOR
是发起反射的类信息,$VICTIM
是被反射的类信息。这种警告源于放宽的强封装机制。
2.3. 放宽的强封装
Java 9 之前,大量第三方库依赖反射访问 JDK 内部 API。如果严格执行强封装,这些库将全部失效。为了平滑迁移到 Java 9 模块系统,Java 团队做了妥协:引入放宽的强封装。
核心机制是启动选项 --illegal-access
,它仅控制"未命名模块 → 平台模块"的反射访问行为,其他场景无效。该选项有四种模式:
模式 | 行为描述 |
---|---|
permit |
默认模式(Java 9-15)。开放平台模块所有包给未命名模块,仅首次访问时显示警告 |
warn |
同 permit ,但每次非法反射访问都显示警告 |
debug |
同 warn ,同时打印堆栈跟踪 |
deny |
禁止所有非法反射访问(Java 16+ 默认模式) |
命令行使用示例:
java --illegal-access=deny com.baeldung.module.unnamed.Main
重要演进:
- Java 9-15:默认
permit
- Java 16:默认
deny
- Java 17+:完全移除
--illegal-access
选项
3. 如何修复反射非法访问
核心原则:在模块系统中,只有被显式打开(open)的包才允许深度反射访问。
3.1. 在模块声明中解决
如果你是代码作者,最直接的方式是在 module-info.java
中声明:
module baeldung.reflected {
opens com.baeldung.reflected.opened; // 对所有模块开放
}
更精确的写法(限定开放范围):
module baeldung.reflected {
opens com.baeldung.reflected.internal to baeldung.intermedium; // 仅对指定模块开放
}
迁移旧代码时,可简单粗暴地开放整个模块:
open module baeldung.reflected {
// 注意:open 模块内部不能再使用 opens 指令
}
3.2. 在命令行解决
如果无法修改源码,可通过命令行临时开放:
--add-opens java.base/java.lang=baeldung.reflecting.named
要开放给所有未命名模块,使用 ALL-UNNAMED
:
java --add-opens java.base/java.lang=ALL-UNNAMED
3.3. 在运行时解决
方式一:使用 Module API
srcModule.addOpens("com.baeldung.reflected.internal", targetModule);
踩坑提醒:Module.addOpens
是调用敏感(caller-sensitive)方法,只有以下情况才能成功调用:
- 被修改的模块自身
- 已被授予开放访问权限的模块
- 未命名模块
否则会抛出
IllegalCallerException
。
方式二:使用 Java Agent
通过 java.instrument
模块的 Instrumentation.redefineModule
方法,可在运行时动态修改模块访问权限:
void redefineModule(Instrumentation inst, Module src, Module target) {
// 准备额外依赖
Set<Module> extraReads = Collections.singleton(target);
// 准备额外导出
Set<String> packages = src.getPackages();
Map<String, Set<Module>> extraExports = new HashMap<>();
for (String pkg : packages) {
extraExports.put(pkg, extraReads);
}
// 准备额外开放
Map<String, Set<Module>> extraOpens = new HashMap<>();
for (String pkg : packages) {
extraOpens.put(pkg, extraReads);
}
// 准备额外 uses 和 provides
Set<Class<?>> extraUses = Collections.emptySet();
Map<Class<?>, List<Class<?>>> extraProvides = Collections.emptyMap();
// 执行模块重定义
inst.redefineModule(src, extraReads, extraExports, extraOpens, extraUses, extraProvides);
}
这段代码会:
- 将
src
模块的所有包导出给target
模块 - 将
src
模块的所有包开放给target
模块 - 建立
src
到target
的依赖关系
4. 结论
本文系统分析了 Java 9+ 模块系统与反射的交互机制:
- 模块系统的强封装通过可读性和可访问性双重规则限制反射
- 不同模块组合下的非法反射访问会产生不同结果(异常或警告)
- 放宽的强封装机制为 Java 8 到 Java 9 的迁移提供了缓冲期
- 解决非法反射访问有三种途径:
- 模块声明中显式开放(推荐)
- 命令行临时开放(适合快速测试)
- 运行时动态开放(适合高级场景)
完整示例代码可在 GitHub 获取。