如何在Java 9上解决无法访问ObjectException(“无法使{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) 抛出 java.lang.ClassFormatError 可访问:模块 java.base 不会将 java.lang“打开 java.lang”到未命名的模块@1941a8ff

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


答案 1

该异常是由 Java 9 中引入的 Java 平台模块系统引起的,特别是其强封装的实现。它只允许在某些条件下访问,最突出的是:

  • 类型必须是公共的
  • 必须导出所属包

同样的限制也适用于反射,导致异常的代码试图使用反射。更准确地说,异常是由调用 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} 不会将 {package}'打开 {B}

到目前为止,最突出的方案是以下两种:

  1. 库或框架使用反射来调用 JDK 模块。在此方案中:

    • {A}是一个 Java 模块(前缀为 orjava.jdk.)
    • {member}并且是 Java API 的一部分{package}
    • {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 中执行,则这可能特别困难。

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

2. 应用程序代码的反射

在这种情况下,您可以编辑反射用于闯入的模块。(如果不是,则您实际上处于案例1中。这意味着命令行标志不是必需的,相反,模块的描述符可以用来打开它的内部。有多种选择:{A}

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

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


答案 2

仍然可以尝试使用较旧的JKD版本。

对于 Eclipse,你需要做 2 件事。转到(G)

  1. 窗口 -> 首选项 -> java -> 编译器将编译器合规性级别设置为特定版本在我的情况下,Eclipse版本设置为JDK 16,我将其恢复为1.8,因为我的代码是用1.8编写的

  2. 窗口 -> 首选项 -> Java > 已安装的 JRE。添加 JRE 安装路径(选择标准 VM))

它对我来说很顺利。