系列中的上一篇
当前教程
使用 --add-exports--add-opens 绕过强封装

系列中的上一篇: 强封装(JDK 内部)

系列中的下一篇: 使用 --add-modules--add-reads 扩展模块图

使用 --add-exports--add-opens 绕过强封装

模块系统对访问内部 API 非常严格:如果包未导出或打开,则会拒绝访问。但包不能仅仅由模块的作者导出或打开 - 还有命令行标志 --add-exports--add-opens,它们允许模块的用户也这样做。

这样,就可以编写和运行访问应用程序依赖项或 JDK API 内部代码的代码。由于这在更多功能或性能(可能)与更低的可维护性或破坏平台完整性之间存在权衡,因此不应轻易做出此决定。并且由于它最终不仅涉及开发人员,还涉及最终应用程序的用户,因此这些命令行标志必须在启动时应用,以便用户知道正在进行权衡。

注意:要完全理解此功能,您需要透彻了解模块系统的几个不同方面,即 其基础对反射的支持限定的 exportsopens如何从命令行构建和启动,以及 为什么强封装很重要

使用 --add-exports 导出包

选项 --add-exports $MODULE/$PACKAGE=$READING_MODULE(适用于 javajavac 命令)将$MODULE$PACKAGE 导出到 $READING_MODULE。因此,$READING_MODULE 中的代码可以访问 $PACKAGE 中的所有公共类型和成员,但其他模块不能。当将 $READING_MODULE 设置为 ALL-UNNAMED 时,类路径中的所有代码都可以访问该包。在不使用模块的项目中,您将始终使用该占位符 - 只有当您自己的代码在模块中运行时,您才能将导出的包限制为特定模块。

--add-exports 后的空格可以用等号 = 替换,这有助于一些工具配置(例如 Maven):--add-exports=.../...=...

在编译时

例如,请参见以下尝试创建内部类 sun.util.BuddhistCalendar 实例的代码

BuddhistCalendar calendar = new BuddhistCalendar();

如果我们这样编译它,我们会在导入或行本身(如果没有导入)得到以下错误

error: package sun.util is not visible
  (package sun.util is declared in module java.base, which does not export it)

选项 --add-exports 可以解决这个问题。如果上面的代码在没有模块声明的情况下编译,我们需要将包打开到 ALL-UNNAMED

javac
    --add-exports java.base/sun.util=ALL-UNNAMED
    Internal.java

如果它在名为 com.example.internal 的模块中,我们可以更精确,从而最大限度地减少内部代码的暴露

javac
    --add-exports java.base/sun.util=com.example.internal
    module-info.java Internal.java

在运行时

在启动代码(在 JDK 17 及更高版本上)时,我们会收到运行时错误

java.lang.IllegalAccessError:
    class Internal (in unnamed module @0x758e9812)
    cannot access class sun.util.BuddhistCalendar (in module java.base)
    because module java.base does not export sun.util to unnamed module @0x758e9812

要解决此问题,我们需要在启动时重复 --add-exports 选项。对于类路径中的代码

java
    --add-exports java.base/sun.util=ALL-UNNAMED
    --class-path com.example.internal.jar
    com.example.internal.Internal

如果它在名为 com.example.internal(定义了主类)的模块中,我们可以再次更精确

java
    --add-exports java.base/sun.util=com.example.internal
    --module-path com.example.internal.jar
    --module com.example.internal

使用 --add-opens 打开包

命令行选项 --add-opens $MODULE/$PACKAGE=$REFLECTING_MODULE$MODULE$PACKAGE 打开到 $REFLECTING_MODULE。因此,$REFLECTING_MODULE 中的代码可以反射地访问 $PACKAGE 中的所有类型和成员,包括公共和非公共成员,但其他模块不能。当将 $READING_MODULE 设置为 ALL-UNNAMED 时,类路径中的所有代码都可以反射地访问该包。在不使用模块的项目中,您将始终使用该占位符 - 只有当您自己的代码在模块中运行时,您才能将打开的包限制为特定模块。

--add-opens 后的空格可以用等号 = 替换,这有助于一些工具配置:--add-opens=.../...=...

由于 --add-opens 与反射绑定,这是一个纯粹的运行时概念,因此它只对 java 命令有意义。但鉴于许多命令行选项在多个工具中都有效,因此在选项有效时报告和解释它很有帮助,因此 javac 不会拒绝该选项,而是发出警告“--add-opens 在编译时无效”。

在运行时

例如,请参见类 Internal 中的以下代码,该代码尝试使用反射创建内部类 sun.util.BuddhistCalendar 的实例

Class.forName("sun.util.BuddhistCalendar").getConstructor().newInstance();

由于代码不编译到内部类 BuddhistCalendar,因此编译在没有其他命令行标志的情况下工作。但在 JDK 17 及更高版本上,执行生成的代码会导致运行时异常

Exception in thread "main" java.lang.IllegalAccessException:
    class Internal cannot access class sun.util.BuddhistCalendar (in module java.base)
    because module java.base does not export sun.util to unnamed module @1f021e6c
        at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:392)
        at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:674)
        at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:489)
        at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)

选项 --add-opens 可以解决这个问题。如果上面的代码在类路径上的 JAR 中,我们需要将包 sun.util 打开到 ALL-UNNAMED

java
    --add-opens java.base/sun.util=ALL-UNNAMED
    --class-path com.example.internal.jar
    com.example.internal.Internal

(回想一下 关于强封装的文章,没有必要打开包 sun.miscsun.reflect,因为它们由 jdk.unsupported 导出。)

如果它在名为 com.example.internal(定义了主类)的模块中,我们可以更精确,从而最大限度地减少内部代码的暴露

java
    --add-opens java.base/sun.util=com.example.internal
    --module-path com.example.internal.jar
    --module com.example.internal

上次更新: 2021 年 9 月 14 日


系列中的上一篇
当前教程
使用 --add-exports--add-opens 绕过强封装

系列中的上一篇: 强封装(JDK 内部)

系列中的下一篇: 使用 --add-modules--add-reads 扩展模块图