JEP 403: Strongly Encapsulate JDK Internals | 严格封装 JDK 内部
摘要
除了 关键内部 API(如 sun.misc.Unsafe
)外,对 JDK 的所有内部元素进行强封装。在 JDK 9 至 JDK 16 中,曾可通过单个命令行选项放宽内部元素的强封装,但现在将不再可能。
历史
本 JEP 是 JEP 396 的后续,JEP 396 将 JDK 从默认的“放宽的强封装”转变为默认的“强封装”,同时允许用户如果需要可以恢复到放宽的状态。本 JEP 的目标、非目标、动机、风险和假设部分与 JEP 396 基本相同,但为方便读者阅读,此处再次列出。
目标
继续提高 JDK 的安全性和可维护性,这是 Project Jigsaw 的主要目标之一。
鼓励开发人员从使用内部元素迁移到使用标准 API,以便他们及其用户能够无缝升级到未来的 Java 版本。
非目标
对于尚未有标准替代品的 JDK 的 关键内部 API,不将其移除、封装或修改。这意味着
sun.misc.Unsafe
将保持不变。不定义新的标准 API 来替换尚未有标准替代品的内部元素,尽管可以针对本 JEP 提出此类 API 的建议。
动机
多年来,各种库、框架、工具和应用程序的开发人员以损害安全性和可维护性的方式使用了 JDK 的内部元素。具体而言:
java.*
包中的一些非public
类、方法和字段定义了特权操作,如 在特定类加载器中定义新类的能力,而其他一些则传递敏感数据,如 加密密钥。尽管这些元素位于java.*
包中,但它们是 JDK 的内部元素。外部代码通过反射使用这些内部元素,会危及平台的安全性。sun.*
包中的所有类、方法和字段都是 JDK 的内部 API。com.sun.*
、jdk.*
和org.*
包中的大多数类、方法和字段也同样是内部 API。这些 API 从未标准化、从未受支持,并且 从未打算用于外部使用。外部代码使用这些内部元素会带来持续的维护负担。花费时间和精力来保留这些 API,以避免破坏现有代码,可能会使平台发展受阻。
在 Java 9 中,我们通过利用模块来 限制对其内部元素的访问,从而提高了 JDK 的安全性和可维护性。模块提供了 强封装,这意味着
模块外部的代码只能访问该模块导出的包的
public
和protected
元素,protected
元素只能从定义它们的类的子类中进行访问。
强封装在编译时和运行时都适用,包括编译后的代码在运行时尝试通过反射访问元素时。导出包的非 public
元素以及未导出包的所有元素都被称为 强封装。
在 JDK 9 及更高版本中,我们对所有新的内部元素进行了强封装,从而限制了对它们的访问。然而,为了帮助迁移,我们故意选择在运行时不对 JDK 8 中已经存在的内部元素进行强封装。因此,类路径上的库和应用程序代码可以继续使用反射来访问 java.*
包的非 public
元素,以及 sun.*
和其他内部包的所有元素(仅限于 JDK 8 中存在的包)。这种安排被称为 宽松的强封装,并且是 JDK 9 中的默认行为。
我们在 2017 年 9 月发布了 JDK 9。现在,JDK 中最常用的内部元素大多已经有了 标准替代品。开发人员已有三年多的时间将 JDK 的内部元素迁移到标准 API,如 java.lang.invoke.MethodHandles.Lookup::defineClass
、java.util.Base64
和 java.lang.ref.Cleaner
。许多库、框架和工具的维护者已经完成了迁移,并发布了更新版本的组件。现在,对宽松强封装的需求比 2017 年时更弱,并且每年都在进一步减弱。
在 2021 年 3 月发布的 JDK 16 中,我们朝着对 JDK 的所有内部元素进行强封装的目标迈出了下一步。JEP 396 将强封装设为默认行为,但保留了如 sun.misc.Unsafe
等关键内部 API 的可用性。在 JDK 16 中,最终用户仍然可以选择宽松的强封装来访问 JDK 8 中存在的内部元素。
现在,我们准备再迈出一步,移除选择宽松强封装的能力。这意味着 JDK 的所有内部元素都将进行强封装,但如 sun.misc.Unsafe
等关键内部 API 除外。
描述
宽松的强封装由启动器选项 --illegal-access
控制。此选项由 JEP 261 引入,其命名具有挑衅性,旨在劝阻使用它。在 JDK 16 及更早版本中,它的工作方式如下:
--illegal-access=permit
安排 JDK 8 中存在的每个包都对未命名模块中的代码 开放。因此,类路径上的代码可以继续使用反射来访问java.*
包的非公开元素,以及 JDK 8 中存在的sun.*
和其他内部包的所有元素。对任何此类元素的第一次反射访问操作会发出警告,但之后不会再次发出警告。此模式是 JDK 9 至 JDK 15 的默认模式。
--illegal-access=warn
与permit
相同,但每次进行非法的反射访问操作时都会发出警告消息。--illegal-access=debug
与warn
相同,但每次进行非法的反射访问操作时都会发出警告消息和堆栈跟踪。--illegal-access=deny
禁用所有非法的访问操作,但其他命令行选项(如--add-opens
)启用的操作除外。此模式是 JDK 16 的默认模式。
作为进一步对 JDK 所有内部元素进行强封装的步骤,我们提议使 --illegal-access
选项过时。无论使用 permit
、warn
、debug
还是 deny
,该选项的使用都不会产生除发出警告消息以外的任何效果。我们预计在未来的版本中完全移除 --illegal-access
选项。
随着这一变更,最终用户将不再能够使用 --illegal-access
选项来启用对 JDK 内部元素的访问。(受影响的包列表可在此处 查看。)sun.misc
和 sun.reflect
包仍将由 jdk.unsupported
模块导出,并且仍将对代码开放,以便通过反射访问它们的非公开元素。 其他 JDK 包将不会以这种方式开放。
仍然可以使用 --add-opens
命令行选项或 JAR 文件的 Add-Opens
清单属性来打开特定的包。
导出的 com.sun
API
JDK 中的大多数 com.sun.*
包都是供内部使用的,但其中一些是支持外部使用的。这些受支持的包在 JDK 9 中已导出,并将继续导出,因此您可以继续针对它们的公共 API 进行编程。然而,它们将不再开放。示例包括
jdk.compiler
模块中的编译器树 API,jdk.httpserver
模块中的 HTTP 服务器 API,jdk.sctp
模块中的 SCTP API,以及jdk.unsupported
模块的com.sun.nio.file
包中针对 JDK 的 NIO API 扩展。
风险与假设
本提案的主要风险在于现有的 Java 代码可能无法运行。可能无法运行的代码类型包括但不限于:
使用
java.lang.ClassLoader
的受保护defineClass
方法来在现有类加载器中定义新类的框架。这些框架应改用自 JDK 9 起可用的java.lang.invoke.MethodHandles.Lookup::defineClass
。使用
sun.util.calendar.ZoneInfo
类来操作时区信息的代码。这些代码应改用自 JDK 8 起可用的java.time
API。使用
com.sun.rowset
包来处理 SQL 行集的代码。这些代码应改用自 JDK 7 起可用的javax.sql.rowset
包。使用
com.sun.tools.javac.*
包来处理源代码的工具。这些工具应改用自 JDK 6 起可用的javax.tools
、javax.lang.model
和com.sun.source.*
API。使用
sun.security.tools.keytool.CertAndKeyGen
类来生成自签名证书的代码。目前尚未有针对此功能的标准 API(尽管已 提交请求);与此同时,开发人员可以使用包含此功能的现有第三方库。使用 JDK 内部版本的 Xerces XML 处理器的代码。这些代码应改用 Xerces 库的独立版本,可从 Maven Central 获取。
使用 JDK 内部版本的 ASM 字节码库的代码。这些代码应改用 ASM 库的独立版本,可从 Maven Central 获取。
我们鼓励所有开发人员:
使用
jdeps
工具来识别依赖于 JDK 内部元素的代码。当 标准替代方案 可用时,请切换到使用这些替代方案。
否则,我们欢迎在 Project Jigsaw 邮件列表 上提出关于新标准 API 的有力案例。但请理解,我们不太可能为未广泛使用的内部元素定义新的标准 API。
使用现有版本(如 JDK 11)通过
--illegal-access=warn
选项测试现有代码,以识别通过反射访问的任何内部元素,然后使用--illegal-access=debug
选项定位错误代码,最后使用--illegal-access=deny
选项进行测试。
次要风险
现有应用程序可能无法运行,不是因为应用程序本身使用了内部 API,而是因为应用程序所使用的库或框架使用了这些内部 API。如果您维护这样的应用程序,我们建议您更新到应用程序所依赖组件的最新版本。如果这些组件尚未更新以去除对内部元素的依赖,我们建议您敦促其维护者进行更新,或者考虑自己完成这项工作并提交补丁。
一些库、框架和工具的维护者告诉应用程序开发人员,在使用 JDK 9 及更高版本时,可以安全地忽略非法的反射访问警告。这导致与总是使用最新 JDK 版本的应用程序开发人员之间产生紧张关系,因为他们意识到,一旦 JDK 的内部元素被严格封装,他们所依赖的组件将立即失效。对于这些应用程序开发人员来说,降级到 JDK 8 或不移至最新版本并不是可行的解决方案。
Exception in thread "main" java.lang.reflect.InaccessibleObjectException:
Unable to make protected java.lang.Class<?> java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int) accessible:
module java.base does not "opens java.lang" to unnamed module @xxxxxxxx
本次变更的影响示例
使用早期版本成功编译但直接访问 JDK 内部 API 的代码将不再工作。例如,
javaSystem.out.println(sun.security.util.SecurityConstants.ALL_PERMISSION);
将引发以下形式的异常:
java线程 "main" 中的异常 java.lang.IllegalAccessError: 类 Test (在模块 @0x5e481248 的未命名模块中) 无法访问类 sun.security.util.SecurityConstants (在模块 java.base 中),因为 模块 java.base 不将 sun.security.util 导出到未命名模块 @0x5e481248
使用反射访问已导出
java.*
API 的private
字段的代码将不再工作。例如,javavar ks = java.security.KeyStore.getInstance("jceks"); var f = ks.getClass().getDeclaredField("keyStoreSpi"); f.setAccessible(true);
将引发以下形式的异常:
java线程 "main" 中的异常 java.lang.reflect.InaccessibleObjectException: 无法使字段 private java.security.KeyStoreSpi java.security.KeyStore.keyStoreSpi 可访问:模块 java.base 不 将 "java.security" 开放给未命名模块 @6e2c634b
使用反射调用已导出
java.*
API 的protected
方法的代码将不再工作。例如,javavar dc = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); dc.setAccessible(true);
将引发以下形式的异常:
java线程 "main" 中的异常 java.lang.reflect.InaccessibleObjectException: 无法使受保护且最终的方法 java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int) throws java.lang.ClassFormatError 可访问:模块 java.base 不 将 "java.lang" 开放给未命名模块 @5e481248