跳到主要内容

JEP 403:强力封装 JDK 内部结构

QWen Max 中英对照 JEP 403: Strongly Encapsulate JDK Internals

总结

强烈封装 JDK 的所有内部元素,关键内部 API(例如 sun.misc.Unsafe)除外。与 JDK 9 到 JDK 16 中的实现不同,不再可能通过单个命令行选项来放宽对内部元素的强封装。

历史

本 JEP 是 JEP 396 的继任者,后者将 JDK 从默认的宽松强封装转变为默认的强封装,同时允许用户根据需要返回到宽松模式。本 JEP 的目标、非目标、动机以及风险和假设部分与 JEP 396 基本相同,但为了方便读者,此处重新列出。

目标

  • 继续提高 JDK 的安全性和可维护性,这是 Project Jigsaw 的主要目标之一。
  • 鼓励开发者从使用内部元素迁移到使用标准 API,这样他们及其用户就可以无障碍地升级到未来的 Java 版本。

非目标

  • 如果某些 JDK 的关键内部 API 尚无标准替代方案,则不以移除、封装或修改这些 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 将强封装设置为了默认行为,但关键的内部 API(例如 sun.misc.Unsafe)仍然可用。在 JDK 16 中,终端用户仍然可以选择放宽强封装,以便访问存在于 JDK 8 中的内部元素。

我们现在准备通过移除选择宽松强封装的能力,在这一旅程中再迈出一步。这意味着除了关键的内部 API(如 sun.misc.Unsafe)之外,JDK 的所有内部元素都将被强封装。

描述

宽松的强封装由启动器选项 --illegal-access 控制。该选项由 JEP 261 引入,其命名颇具挑衅性,目的是为了不鼓励使用。在 JDK 16 及更早版本中,其作用如下:

  • --illegal-access=permit 会将 JDK 8 中存在的每个包都开放给未命名模块中的代码。因此,类路径上的代码可以继续使用反射访问 java.* 包中的非公共元素,以及 sun.* 和其他内部包中的所有元素(前提是这些包在 JDK 8 中存在)。对任何此类元素的首次反射访问操作会触发警告,但在那之后不会再发出警告。

    此模式从 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 命令行选项,或者 Add-Opens JAR 文件清单属性来开放特定的包。

导出的 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 包中对 NIO API 的 JDK 特定扩展。

风险与假设

此提案的主要风险是现有 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,而是因为它使用的库或框架这样做了。如果你维护这样的应用程序,我们建议你更新到你的应用程序所依赖的组件的最新版本。如果这些组件还没有更新以去除对内部元素的依赖,我们建议你敦促其维护者这样做,或者考虑自己进行这项工作并提交补丁。

  • 一些库、框架和工具的维护者一直在告诉应用程序开发者,在使用 JDK 9 及更高版本时,可以安全地忽略非法的反射访问警告。这与那些始终使用最新 JDK 版本的应用程序开发者产生了矛盾,他们意识到一旦 JDK 的内部元素被强烈封装,他们所依赖的组件就会崩溃。对于这些应用程序开发者来说,降级到 JDK 8 或不迁移到最新版本并不是一个可行的方法。

此更改的影响示例

  • 以前版本中能够成功编译并且直接访问 JDK 内部 API 的代码将不再起作用。例如,

    System.out.println(sun.security.util.SecurityConstants.ALL_PERMISSION);
    java

    将会抛出如下形式的异常

    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
  • 使用反射访问已导出 java.* API 的 private 字段的代码将不再起作用。例如,

    var ks = java.security.KeyStore.getInstance("jceks");
    var f = ks.getClass().getDeclaredField("keyStoreSpi");
    f.setAccessible(true);
    java

    将会抛出如下形式的异常

    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
  • 使用反射调用已导出 java.* API 的 protected 方法的代码将不再起作用。例如,

    var dc = ClassLoader.class.getDeclaredMethod("defineClass",
    String.class,
    byte[].class,
    int.class,
    int.class);
    dc.setAccessible(true);
    java

    将会抛出如下形式的异常

    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
    java