JEP 396: 默认强封装 JDK 内部构件
总结
默认情况下,强烈封装 JDK 的所有内部元素,关键内部 API(例如 sun.misc.Unsafe
)除外。允许最终用户选择自 JDK 9 以来默认的宽松强封装。
目标
-
继续提升 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 中已存在的包内容进行强封装。因此,类路径上的库和应用程序代码仍然可以通过反射访问 JDK 8 中已存在包的 java.*
包中的非 public
元素,以及 sun.*
和其他内部包的所有元素。这种安排被称为 宽松的强封装。
我们在 2017 年 9 月发布了 JDK 9。现在,JDK 中大部分常用的内部元素都有了标准替代方案。开发者们已经拥有超过三年的时间,可以将使用从 JDK 的内部元素迁移到标准 API 上,例如 java.lang.invoke.MethodHandles.Lookup::defineClass
、java.util.Base64
和 java.lang.ref.Cleaner
。许多库、框架和工具的维护者已经完成了迁移,并发布了其组件的更新版本。现在,我们准备按照 Project Jigsaw 最初的计划,朝着完全封装所有 JDK 内部元素(除了关键内部 API,如 sun.misc.Unsafe
)的方向迈出下一步。
描述
宽松的强封装由启动器选项 --illegal-access
控制。该选项由 JEP 261 引入,其命名颇具挑衅性,目的是为了不鼓励使用它。目前它的作用如下:
-
--illegal-access=permit
会将 JDK 8 中存在的每个包都开放给未命名模块中的代码。因此,类路径上的代码可以继续使用反射访问java.*
包中的非公共元素,以及sun.*
和其他内部包的所有元素(仅限于 JDK 8 中存在的包)。对任何此类元素的第一次反射访问操作都会触发警告,但在那之后不会再发出警告。自 JDK 9 起,这种模式一直是默认设置。
-
--illegal-access=warn
与permit
类似,但每次进行非法的反射访问操作时都会发出警告信息。 -
--illegal-access=debug
与warn
类似,但每次进行非法的反射访问操作时,不仅会发出警告信息,还会生成堆栈跟踪。 -
--illegal-access=deny
会禁用所有非法访问操作,除非通过其他命令行选项启用,例如--add-opens
。
我们还将修改 Java 平台规范 中的相关文本,禁止在任何 Java 平台实现中默认打开任何包,除非该包在其包含模块的声明中被显式声明为 open
。
--illegal-access
选项的 permit
、warn
和 debug
模式将继续有效。这些模式允许最终用户根据需要选择较为宽松的强封装。
我们预计未来的 JEP 将完全移除 --illegal-access
选项。到那时,将无法通过一个命令行选项打开所有的 JDK 8 包。仍然可以使用 --add-opens
命令行选项,或者使用 Add-Opens
JAR 文件属性来打开特定的包。
为了准备最终移除 --illegal-access
选项,我们将根据此 JEP(Java 增强提案)将其标记为过时以待移除。因此,向 java
启动器指定该选项将会触发一条过时警告。
风险与假设
此提案的主要风险在于现有的 Java 代码可能无法运行。可能失败的代码类型包括但不限于:
-
使用
java.lang.ClassLoader
的protected
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 处理器副本的代码。此类代码应改为使用从 Maven Central 获取的独立 Xerces 库。
-
使用 JDK 内部版本的 ASM 字节码库的代码。此类代码应改为使用从 Maven Central 获取的独立 ASM 库。
我们鼓励所有开发者:
-
使用
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 -
默认情况下,使用反射访问已导出
java.*
API 的private
字段的代码将不再有效。例如,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 -
默认情况下,使用反射调用已导出
java.*
API 的protected
方法的代码将不再有效。例如,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