Skip to content

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 的安全性和可维护性。模块提供了 强封装,这意味着

  • 模块外部的代码只能访问该模块导出的包的 publicprotected 元素,

  • 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::defineClassjava.util.Base64java.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=warnpermit 相同,但每次进行非法的反射访问操作时都会发出警告消息。

  • --illegal-access=debugwarn 相同,但每次进行非法的反射访问操作时都会发出警告消息和堆栈跟踪。

  • --illegal-access=deny 禁用所有非法的访问操作,但其他命令行选项(如 --add-opens)启用的操作除外。

    此模式是 JDK 16 的默认模式。

作为进一步对 JDK 所有内部元素进行强封装的步骤,我们提议使 --illegal-access 选项过时。无论使用 permitwarndebug 还是 deny,该选项的使用都不会产生除发出警告消息以外的任何效果。我们预计在未来的版本中完全移除 --illegal-access 选项。

随着这一变更,最终用户将不再能够使用 --illegal-access 选项来启用对 JDK 内部元素的访问。(受影响的包列表可在此处 查看。)sun.miscsun.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.toolsjavax.lang.modelcom.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 或不移至最新版本并不是可行的解决方案。

java
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 的代码将不再工作。例如,

    java
    System.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 字段的代码将不再工作。例如,

    java
    var 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 方法的代码将不再工作。例如,

    java
    var 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