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 模块系统包含多种模块类型(命名模块、未命名模块、平台模块等),它们之间的反射访问规则各不相同。我们重点分析三种典型的非法反射访问场景:

  1. 命名模块 → 命名模块

    • 使用深度反射(调用 setAccessible(true) 访问非公有成员)
    • 结果:抛出 IllegalAccessExceptionInaccessibleObjectException
  2. 未命名模块 → 应用命名模块

    • 同样使用深度反射
    • 结果:同样抛出上述异常
  3. 未命名模块 → 平台模块

    • 使用深度反射访问 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);
}

这段代码会:

  1. src 模块的所有包导出给 target 模块
  2. src 模块的所有包开放给 target 模块
  3. 建立 srctarget 的依赖关系

4. 结论

本文系统分析了 Java 9+ 模块系统与反射的交互机制:

  1. 模块系统的强封装通过可读性和可访问性双重规则限制反射
  2. 不同模块组合下的非法反射访问会产生不同结果(异常或警告)
  3. 放宽的强封装机制为 Java 8 到 Java 9 的迁移提供了缓冲期
  4. 解决非法反射访问有三种途径:
    • 模块声明中显式开放(推荐)
    • 命令行临时开放(适合快速测试)
    • 运行时动态开放(适合高级场景)

完整示例代码可在 GitHub 获取。


原始标题:Java 9 Illegal Reflective Access Warning