使用 --add-exports
和 --add-opens
绕过强封装
模块系统对访问内部 API 非常严格:如果包未导出或打开,则会拒绝访问。但包不能仅仅由模块的作者导出或打开 - 还有命令行标志 --add-exports
和 --add-opens
,它们允许模块的用户也这样做。
这样,就可以编写和运行访问应用程序依赖项或 JDK API 内部代码的代码。由于这在更多功能或性能(可能)与更低的可维护性或破坏平台完整性之间存在权衡,因此不应轻易做出此决定。并且由于它最终不仅涉及开发人员,还涉及最终应用程序的用户,因此这些命令行标志必须在启动时应用,以便用户知道正在进行权衡。
注意:要完全理解此功能,您需要透彻了解模块系统的几个不同方面,即 其基础、对反射的支持、限定的 exports
和 opens
、如何从命令行构建和启动,以及 为什么强封装很重要。
使用 --add-exports
导出包
选项 --add-exports $MODULE/$PACKAGE=$READING_MODULE
(适用于 java
和 javac
命令)将$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.misc
和 sun.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 日