如何解决 Java 9 上的 InaccessibleObjectException(“无法使 {member} 可访问:模块 {A} 不‘打开 {package}’到 {B}”)?

新手上路,请多包涵

在 Java 9 上运行应用程序时,这种异常发生在各种场景中。某些库和框架(Spring、Hibernate、JAXB)特别容易出现这种情况。这是来自 Javassist 的示例:

 java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @1941a8ff
    at java.base/jdk.internal.reflect.Reflection.throwInaccessibleObjectException(Reflection.java:427)
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:201)
    at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:192)
    at java.base/java.lang.reflect.Method.setAccessible(Method.java:186)
    at javassist.util.proxy.SecurityActions.setAccessible(SecurityActions.java:102)
    at javassist.util.proxy.FactoryHelper.toClass2(FactoryHelper.java:180)
    at javassist.util.proxy.FactoryHelper.toClass(FactoryHelper.java:163)
    at javassist.util.proxy.ProxyFactory.createClass3(ProxyFactory.java:501)
    at javassist.util.proxy.ProxyFactory.createClass2(ProxyFactory.java:486)
    at javassist.util.proxy.ProxyFactory.createClass1(ProxyFactory.java:422)
    at javassist.util.proxy.ProxyFactory.createClass(ProxyFactory.java:394)

消息说:

无法使受保护的最终 java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError 可访问:模块 java.base不会“打开 java.lang”到未命名的模块 @1941a8ff

可以做些什么来避免异常并让程序成功运行?

原文由 Nicolai Parlog 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 3.4k
2 个回答

该异常是由 Java 9 中引入的 Java Platform Module System 引起的,特别是它对强封装的实现。它只允许在某些条件下 访问,最突出的是:

  • 类型必须是公开的
  • 必须导出拥有的包

同样的限制也适用于反射,导致异常的代码试图使用反射。更准确地说,异常是由调用 setAccessible 引起的。这可以在上面的堆栈跟踪中看到,其中 javassist.util.proxy.SecurityActions 中的相应行如下所示:

 static void setAccessible(final AccessibleObject ao,
                          final boolean accessible) {
    if (System.getSecurityManager() == null)
        ao.setAccessible(accessible); // <~ Dragons
    else {
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                ao.setAccessible(accessible);  // <~ moar Dragons
                return null;
            }
        });
    }
}

为了确保程序成功运行,必须说服模块系统允许访问调用了 setAccessible 的元素。所需的所有信息都包含在异常消息中,但有 许多机制 可以实现这一点。哪个是最好的取决于导致它的确切情况。

无法使 {member} 可访问:模块 {A} 未向 {B}“打开 {package}”

到目前为止,最突出的场景有以下两种:

  1. 库或框架使用反射来调用 JDK 模块。在这种情况下:

    • {A} 是一个 Java 模块(前缀为 java.jdk.
    • {member}{package} 是 Java API 的一部分
    • {B} 是库、框架或应用程序模块;经常 unnamed module @...
  2. 像 Spring、Hibernate、JAXB 等基于反射的库/框架……通过应用程序代码反射来访问 bean、实体……在这种情况下:

    • {A} 是一个应用模块
    • {member}{package} 是应用程序代码的一部分
    • {B} 是框架模块或 unnamed module @...

请注意,某些库(例如 JAXB)可能在两个帐户上都失败,因此请仔细查看您所处的场景!问题中的一个是案例1。

1.对JDK的反射调用

JDK 模块对于应用程序开发人员来说是不可变的,因此我们无法更改它们的属性。这只剩下一种可能的解决方案: 命令行标志。有了它们,就可以打开特定的包进行反射。

因此,在上述情况下(缩短)…

无法使 java.lang.ClassLoader.defineClass 可访问:模块 java.base 不会“打开 java.lang”到未命名的模块 @1941a8ff

…正确的解决方法是按如下方式启动 JVM:

 # --add-opens has the following syntax: {A}/{package}={B}
java --add-opens java.base/java.lang=ALL-UNNAMED

如果反射代码在命名模块中, ALL-UNNAMED 可以替换为其名称。

请注意,有时很难找到一种方法将此标志应用于实际执行反射代码的 JVM。如果有问题的代码是项目构建过程的一部分并且在构建工具产生的 JVM 中执行,这可能会特别困难。

如果要添加的标志太多,您可以考虑使用 封装终止开关 --permit-illegal-access 。它将允许类路径上的所有代码反映整个命名模块。请注意,此标志 仅适用于 Java 9

2. 对应用程序代码的反思

在这种情况下,您可能可以编辑反射用于闯入的模块。 (如果不是,那么您实际上是情况 1。)这意味着不需要命令行标志,而是可以使用模块 {A} 的描述符来打开其内部。有多种选择:

  • 使用 exports {package} 导出包,使其在编译和运行时可用于所有代码
  • 使用 exports {package} to {B} 将包导出到访问模块,这使得它在编译和运行时可用,但仅限于 {B}
  • opens {package} 打开包,这使它在运行时(有或没有反射)对所有代码可用
  • 使用 opens {package} to {B} 将包打开到访问模块,这使得它在运行时可用(有或没有反射)但仅限于 {B}
  • open module {A} { ... } 打开整个模块,这使得它的所有包在运行时(有或没有反射)对所有代码都可用

有关这些方法的更详细讨论和比较,请参阅 这篇文章

原文由 Nicolai Parlog 发布,翻译遵循 CC BY-SA 4.0 许可协议

只是最近的反馈

许多解决此问题的建议都与 vm 启动器选项 --illegal-access

根据 Oracle 的说法, JEP 403 (link1)JEP 403 (link2) 已决定 从 JDK 17 及以后版本交付,启动器选项 --illegal-access 将停止工作!

总结 强烈封装 JDK 的所有内部元素,除了关键的内部 API,如 sun.misc.Unsafe。不再可能通过单个命令行选项放松对内部元素的强封装,这在 JDK 9 到 JDK 16 中是可能的。

通过此更改, 最终用户将无法再使用 –illegal-access 选项 来启用对 JDK 内部元素的访问。 (受影响的包列表可在此处获得。)sun.misc 和 sun.reflect 包仍将由 jdk.unsupported 模块导出,并且仍将打开,以便代码可以通过反射访问它们的非公共元素。不会以这种方式打开其他 JDK 包。

仍然可以使用 –add-opens 命令行选项或 Add-Opens JAR 文件清单属性来打开特定包。

所以下面的解决方案将继续工作

# --add-opens has the following syntax: {A}/{package}={B}
java --add-opens java.base/java.lang=ALL-UNNAMED

但是 --illegal-access 的解决方案将从 JDK 17 开始停止工作。

原文由 Panagiotis Bougioukos 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题