Skip to content

JEP 396: Strongly Encapsulate JDK Internals by Default | 默认情况下严格封装 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 的安全性和可维护性。模块提供了 强封装 功能,这意味着

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

  • 并且 protected 元素只能从定义它们的类的子类中进行访问。

强封装在编译时和运行时都适用,包括在编译的代码尝试通过反射在运行时访问元素时。导出包的非 public 元素以及未导出包的所有元素都被视为 强封装

在 JDK 9 及后续版本中,我们对所有新的内部元素进行了强封装,从而限制了对它们的访问。然而,为了帮助迁移,我们故意选择在运行时不对 JDK 8 中存在的包的内容进行强封装。因此,类路径上的库和应用程序代码可以继续使用反射来访问 java.* 包的非 public 元素,以及 sun.* 和其他内部包(JDK 8 中存在的包)的所有元素。这种安排被称为 宽松的强封装

我们在 2017 年 9 月发布了 JDK 9。JDK 中大多数常用的内部元素现在都有 标准替代品。开发人员已有三年多的时间将 JDK 的内部元素迁移到标准 API,如 java.lang.invoke.MethodHandles.Lookup::defineClassjava.util.Base64java.lang.ref.Cleaner。许多库、框架和工具维护人员已完成迁移,并发布了其组件的更新版本。我们现在准备按照 Project Jigsaw 的原始计划,对 JDK 所有内部元素(除关键内部 API 如 sun.misc.Unsafe 外)采取下一步强封装措施。

说明

宽松的强封装由启动器选项 --illegal-access 控制。此选项由 JEP 261 引入,命名具有挑衅性,以劝阻其使用。它目前的工作方式如下:

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

自 JDK 9 以来,此模式一直是默认模式。

  • --illegal-access=warnpermit 相同,但每次非法反射访问操作都会发出警告消息。

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

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

作为对 JDK 所有内部元素进行强封装的下一步,我们提议将 --illegal-access 选项的默认模式从 permit 更改为 deny。进行此更改后,JDK 8 中存在的且不包含 关键内部 API 的包将不再默认开放;完整列表可在 此处 找到。sun.misc 包仍将由 jdk.unsupported 模块导出,并且仍然可以通过反射进行访问。

我们还将修订 Java Platform Specification 中的相关文本,以禁止在任何 Java Platform Implementation 中默认打开任何包,除非在包含该包的模块的声明中明确声明该包为 open

--illegal-access 选项的 permitwarndebug 模式将继续有效。这些模式允许最终用户(如果愿意)选择宽松的强封装。

我们预计未来的 JEP 将完全移除 --illegal-access 选项。到那时,将无法通过单个命令行选项打开所有 JDK 8 包。但用户仍可以使用 --add-opens 命令行选项或 Add-Opens JAR 文件属性来打开特定包。

为了准备最终移除 --illegal-access 选项,我们将在此 JEP 中将其标记为弃用以便移除。因此,向 java 启动器指定此选项将导致发出弃用警告。

风险与假设

本提案的主要风险是现有的 Java 代码将无法运行。将失败的代码类型包括但不限于:

  • 使用 java.lang.ClassLoaderprotected 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 或不移至最新版本并不是可行的解决方案。

此次更改的影响示例

  • 使用早期版本成功编译的、直接访问 JDK 内部 API 的代码将默认无法运行。例如,

    java
    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.* API 中 private 字段的代码将默认无法运行。例如,

    java
    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.* API 中 protected 方法的代码将默认无法运行。例如,

    java
    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