JEP 403:强烈封装 JDK 内部结构
概括
严格封装 JDK 的所有内部元素,除了关键的内部 API,例如sun.misc.Unsafe
.将不再可能通过单个命令行选项来放松内部元素的强封装,这在 JDK 9 到 JDK 16 中是可能的。
历史
这个 JEP 是JEP 396的后继者,它将 JDK 从默认 的_宽松强封装_转变为默认的_强封装_,同时允许用户根据需要返回到宽松的姿势。本 JEP 的目标、非目标、动机以及风险和假设部分本质上与 JEP 396 的部分相同,但在此复制是为了方便读者。
目标
-
鼓励开发人员从使用内部元素迁移到使用标准 API,以便他们及其用户可以轻松升级到未来的 Java 版本。
非目标
-
删除、封装或修改尚不存在标准替代品的 JDK 的任何关键内部 API并不是我们的目标。这意味着
sun.misc.Unsafe
将仍然可用。 -
定义新的标准 API 来替换尚不存在标准替换的内部元素并不是目标,尽管可以建议此类 API 来响应此 JEP。
动机
多年来,各种库、框架、工具和应用程序的开发人员以损害安全性和可维护性的方式使用 JDK 的内部元素。尤其:
-
包的一些非
public
类、方法和字段java.*
定义特权操作,例如在特定类加载器中定义新类的能力,而其他一些则传递敏感数据,例如加密密钥。尽管这些元素位于包中,但它们是 JDK 的内部元素java.*
。外部代码通过反射使用这些内部元素会使平台的安全面临风险。 -
包的所有类、方法和字段
sun.*
都是 JDK 的内部 API。com.sun.*
、jdk.*
和包的大多数类、方法和字段org.*
也是内部 API。这些 API 从来都不是标准的,从未受支持,也从未打算供外部使用。外部代码对这些内部元素的使用是一项持续的维护负担。花在维护这些 API 上的时间和精力,以免破坏现有代码,可以更好地用于推动平台向前发展。
在 Java 9 中,我们通过利用模块来限制对其内部元素的访问,从而提高了 JDK 的安全性和可维护性。模块提供了_强大的封装性_,这意味着
-
模块外部的代码只能访问该模块导出的包的
public
和元素,并且protected
-
protected
此外,元素只能从定义它们的类的子类中访问。
强封装适用于编译时和运行时,包括编译后的代码尝试在运行时通过反射访问元素时。导出包的非public
元素以及未导出包的所有元素都被认为是_强封装的_。
在 JDK 9 及更高版本中,我们强烈封装了所有新的内部元素,从而限制了对它们的访问。然而,作为迁移的辅助手段,我们特 意选择在运行时不强烈封装 JDK 8 中存在的内部元素。因此,类路径上的库和应用程序代码可以继续使用反射来访问非元素public
。对于 JDK 8 中存在的java.*
包,以及包的所有元素和sun.*
其他内部包。这种安排称为宽松的强封装,并且是 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将强封装作为默认行为,但关键的内部 API(例如sun.misc.Unsafe
仍然可用)除外。在 JDK 16 中,最终用户仍然可以选择宽松的强封装,以便访问 JDK 8 中存在的内部元素。
我们现在准备在这一旅程中再迈出一步,取消选择宽松强封装的能力。这意味着除了关键的内部 API(例如sun.misc.Unsafe
.
描述
宽松的强封装由启动器选项控制--illegal-access
。该选项由JEP 261引入,其命名具有挑衅性,以阻止其使用。在 JDK 16 及更早版本中,它的工作原理如下:
-
--illegal-access=permit
安排 JDK 8 中存在的每个包对未命名模块中的代码开放。因此,对于 JDK 8 中存在的包,类路径上的代码可以继续使用反射来访问包的非公共元素java.*
以及包的所有元素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
命令行选项或Add-Opens
JAR 文件清单属性来打开特定包。
导出的com.sun
API
com.sun.*
JDK 中的大多数包都是供内部使用的,但也有少数支持供外部使用。这些受支持的包已在 JDK 9 中导出,并将继续导出,因此您可以继续针对其公共 API 进行编程。然而,它们将不再开放。例子包括
- 模块中的编译器树 API
jdk.compiler
, - 模块中的 HTTP Server API
jdk.httpserver
, - 模块中的 SCTP API
jdk.sctp
,以及 com.sun.nio.file
模块包中 NIO API 的 JDK 特定扩展jdk.unsupported
。
风险和假设
该提案的主要风险是现有的 Java 代码将无法运行。失败的代码类型包括但不限于:
-
使用方法在现有类加载器中定义新类
protected
defineClass
的框架。java.lang.ClassLoader
此类框架应该使用java.lang.invoke.MethodHandles.Lookup::defineClass
JDK 9 以来提供的 。 -
sun.util.calendar.ZoneInfo
使用该类操作时区信息的代码。此类代码应使用java.time
自 JDK 8 起可用的 API。 -
com.sun.rowset
使用该包处理 SQL 行集的代码。此类代码应使用javax.sql.rowset
自 JDK 7 起可用的包。 -
com.sun.tools.javac.*
使用包来处理源代码的工具。此类工具应改为使用自 JDK 6 起可用的javax.tools
、javax.lang.model
和API。com.sun.source.*
-
sun.security.tools.keytool.CertAndKeyGen
使用该类生成自签名证书的代码。目前还没有针对此功能的标准 API(尽管已经提交了请求);同时,开发人员可以使用包含此功能的现有第三方库。 -
使用 Xerces XML 处理器的 JDK 内部副本的代码。此类代码应使用 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,而是因为应用程序使用了这样做的库或框架。如果您维护此类应用程序,那么我们建议您更新到应用程序所依赖的组件的最新版本。如果这些组件尚未更新以消除对内部元素的依赖关系,那么我们建议您敦促其维护人员这样做,或者考虑自己完成这项工作并提交补丁。
-
一些库、框架和工具的维护者一直告诉应用程序开发人员,在使用 JDK 9 及更高版本时,可以安全地忽略非法反射访问警告。这会导致应用程序开发人员感到紧张,他们总是使用最新的 JDK 版本,并意识到一旦 JDK 的内部元素被严格封装,他们所依赖的组件就会崩溃。对于这些应用程序开发人员来说,降级到 JDK 8 或不迁移到最新版本并不是一个可行的方法。
此更改的影响示例
-
使用早期版本成功编译并直接访问 JDK 内部 API 的代码将不再有效。例如,
System.out.println(sun.security.util.SecurityConstants.ALL_PERMISSION);
将会失败,但形式除外
Exception in thread "main" java.lang.IllegalAccessError: class Test
(in unnamed module @0x5e481248) cannot access class
sun.security.util.SecurityConstants (in module java.base) because
module java.base does not export sun.security.util to unnamed
module @0x5e481248 -
使用反射访问
private
导出java.*
API 字段的代码将不再有效。例如,var ks = java.security.KeyStore.getInstance("jceks");
var f = ks.getClass().getDeclaredField("keyStoreSpi");
f.setAccessible(true);将会失败,但形式除外
Exception in thread "main" java.lang.reflect.InaccessibleObjectException:
Unable to make field private java.security.KeyStoreSpi
java.security.KeyStore.keyStoreSpi accessible: module java.base does
not "opens java.security" to unnamed module @6e2c634b -
protected
使用反射调用导出 API方法的代码java.*
将不再有效。例如,var dc = ClassLoader.class.getDeclaredMethod("defineClass",
String.class,
byte[].class,
int.class,
int.class);
dc.setAccessible(true);将会失败,但形式除外
Exception in thread "main" java.lang.reflect.InaccessibleObjectException:
Unable to make protected final java.lang.Class
java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int)
throws java.lang.ClassFormatError accessible: module java.base does
not "opens java.lang" to unnamed module @5e481248