跳到主要内容

JEP 238:多版本 JAR 文件

概括

扩展 JAR 文件格式以允许多个 Java 发行版特定版本的类文件共存于单个存档中。

目标

  1. 增强 Java Archive Tool ( jar),使其可以创建多版本 JAR 文件。

  2. 在 JRE 中实现多版本 JAR 文件,包括标准类加载器和JarFileAPI 中的支持。

  3. 增强其他关键工具(例如,javacjavapjdeps等)来解释多版本 JAR 文件。

  4. 支持目标 1 至 3 的多版本模块化JAR 文件。

  5. 保持性能:使用多版本 JAR 文件的工具和组件的性能不得受到显着影响。特别是,访问普通(即非多版本)JAR 文件时的性能不得降低。

动机

第三方库和框架通常支持一系列 Java 平台版本,通常可以追溯到几个版本。因此,它们通常不会利用新版本中可用的语言或 API 功能,因为很难表达条件平台依赖性(这通常涉及反射)或为不同平台版本分发不同的库工件。

这会阻碍库和框架使用新功能,进而阻碍用户升级到新的 JDK 版本——这是一个阻碍采用的恶性循环,对每个人都不利。

此外,一些库和框架使用 JDK 的内部 API,当严格执行模块边界时,这些 API 在 Java 9 中将无法访问。当此类内部 API 有公共的、受支持的 API 替代品时,这也会阻碍支持新平台版本。

描述

JAR 文件具有一个内容根(其中包含类和资源)以及一个META-INF目录(其中包含有关 JAR 的元数据)。通过将一些版本控制元数据添加到特定的文件组,JAR 格式可以以兼容的方式为不同目标 Java 平台版本编码库的多个版本。

多版本 JAR(“MRJAR”)将包含主要属性:

Multi-Release: true

在 JAR 的主要部分中声明MANIFEST.MF。属性名称也被声明为常量java.util.jar.Attributes.MULTI_RELEASE。与其他主要属性一样,在 中声明的名称MANIFEST.MF不区分大小写。该值也不区分大小写,但前面或后面不能有空格(这样的限制有助于确保满足性能目标)。

多版本 JAR(“MRJAR”)将包含特定于特定 Java 平台版本的类和资源的附加目录。典型库的 JAR 可能如下所示:

jar root
- A.class
- B.class
- C.class
- D.class

假设 A 和 B 有可以利用 Java 9 功能的替代版本。我们可以将它们捆绑到一个 JAR 中,如下所示:

jar root
- A.class
- B.class
- C.class
- D.class
- META-INF
- versions
- 9
- A.class
- B.class

在不支持MRJAR的JDK中,只有根目录中的类和资源是可见的,并且这两个包将无法区分。在支持 MRJAR 的 JDK 中,与任何更高版本的 Java 平台版本相对应的目录将被忽略;它会首先在与当前运行的主要 Java 平台发行版本相对应的 Java 平台特定目录中搜索类和资源,然后搜索较低版本的类和资源,最后搜索 JAR 根目录。在 Java 9 JDK 上,就好像存在一个特定于 JAR 的类路径,首先包含版本 9 文件,然后包含 JAR 根;在 Java 8 JDK 上,此类路径将仅包含 JAR 根。

假设稍后 Java 10 发布,并且 A 已更新以利用 Java 10 功能。 MRJAR 可能如下所示:

jar root
- A.class
- B.class
- C.class
- D.class
- META-INF
- versions
- 9
- A.class
- B.class
- 10
- A.class

通过这种方案,为较新的 Java 平台版本设计的类的版本可以覆盖为较早的 Java 平台版本设计的同一类的版本。在上面的示例中,当在支持 MRJAR 的 Java 9 JDK 上运行时,它将看到 A 和 B 的 9 特定版本以及 C 和 D 的通用版本;在未来支持 MRJAR 的 Java 10 JDK 上,它将看到 A 的 10 特定版本和 B 的 9 特定版本;在较旧的或不支持 MRJAR 的 JDK 上,它只会看到所有版本的根版本。

JAR 元数据(例如在文件MANIFEST.MFMETA-INF/services目录中找到的元数据)不需要进行版本控制。 MRJAR 本质上是一个发布单元,因此它只有一个发布版本(这与普通 JAR 没有什么不同,例如通过 Maven Central 分发),尽管它内部包含一个库实现的多个版本以在不同的 Java 平台上使用发布。每个版本的库都应该提供相同的 API;需要进行调查以确定这是否应该是 API 完全相同(字节码签名相等)的严格向后兼容性,或者是否可以在某种程度上放宽这一点,而不必引入新的增强功能,从而模糊一个概念发布单位。这可能至少意味着特定于版本的目录中存在的公共类也应该存在于根中,尽管它不需要存在于早期版本目录中。运行时系统不会验证此属性,但工具可以而且应该检测此类 API 兼容性问题,并且还可以提供库方法来执行此类验证(例如 on java.util.jar.JarFile)。

最终,这种机制使库和框架开发人员能够将特定 Java 平台发行版本中 API 的使用与所有用户迁移到该版本的要求分离开来。库和框架维护人员可以逐渐迁移到并支持新功能,同时仍然支持旧功能,打破先有鸡还是先有蛋的循环,以便库可以“Java 9 就绪”,而实际上不需要 Java 9。

细节

为了支持多版本 JAR 文件,将修改 JDK 的以下组件。

  • 基于 JAR 的URLClassLoader必须读取由运行的 Java 平台版本指示的选定版本的类文件。 Project Jigsaw 引入的基于模块的类加载器将需要类似的修改。

  • jarURL 方案和类的协议处理程序java.util.jar.JarFile必须从多版本 JAR 中选择适当版本的类。

  • Java 编译器 ( javac) 必须通过底层JavacFileManagerAPI读取由和命令行选项ZipFileSystem指定的选定版本的类文件。工具、和将利用和 的基本更改。-target``-release``javah``schemagen``wsgen``JavacFileManager``ZipFileSystem

  • Java Archive 工具 ( jar) 将得到增强,以便它可以创建多版本 JAR 文件。

  • 必须更新JAR 打包工具 ( pack200/ )(请参阅JDK-8066272)。unpack200

  • 必须更新该javap工具才能选择版本化类文件。

  • jdeps工具将需要修改以显示版本信息并遵循版本特定的类文件依赖性。

  • 必须修改 JAR 规范以描述多版本 JAR 文件格式和任何相关更改(例如,可能添加到清单中)。

兼容性

java.util.jar.JarFile默认情况下,方案协议处理程序的行为jar将保持不变。有必要选择构建一个JarFile指向 MRJAR 的指针来选择条目的版本。同样,有必要选择jarURL(有关详细信息,请参阅下一节)。

JarFile运行时为类加载创建的实例将选择加入并创建配置为根据运行的 Java 平台的版本选择条目的实例。例如,JarFile实例被称为运行时版本控制。

类加载器资源

由类加载器生成的资源 URL,用于标识 MRJAR 中的资源,将直接引用版本化条目(如果存在)。例如,对于版本化资源,foo/baz/resource.txt

URL r = loader.getResource("foo/baz/resource.txt");

URL“r”可能是:

jar:file:/mrjar.jar!/META-INF/versions/9/foo/baz/resource.txt

而不是:

jar:file:/mrjar.jar!/foo/baz/resource.txt

这种方法被认为是破坏性最小的选择。更改资源 URL 的结构并非没有风险(例如新方案或附加片段)。遗留代码可能会直接处理 URL 字符,而不是解析 URL 并正确提取组件。虽然这样的 URL 处理是不正确的,但人们认为它比不破坏这样的代码更好。

模块化多版本 JAR 文件

模块化多版本 JAR 文件是一个多版本 JAR 文件,module-info.class在顶部的根目录中具有模块描述符 ,就像模块化 JAR 文件一样(请参阅JEP 261 的打包:模块化 JAR部分)。此外,模块化描述符可能存在于版本化区域中。此类版本描述符必须与根模块描述符相同,但有两个例外:

  • 版本化描述符可以有不同的非transitive requires子句java.*jdk.*模块;和

  • 版本化描述符可以有不同的子句,甚至是在模块uses外部定义的服务类型。java.*``jdk.*

这里的原因是,这些是实现细节,而不是模块 API 表面的一部分,并且人们很可能希望随着 JDK 本身的发展而更改它们。requires不允许更改非公开的非 JDK 模块。如果这是必要的,则需要新版本的模块(至少增加其版本号),这是另一种兼容性问题,并且超出了 MRJAR 的范围。

多版本模块不需要在定位的根处有模块描述符。在这方面,模块描述符的处理方式与任何其他类或资源文件没有什么不同。例如,这可以确保根区域中仅存在 Java 8 版本化类,而第 9 版本化区域中存在 Java 9 版本化类(包括模块描述符)。

类路径和模块路径

可以构建模块化 JAR,使其在 Java 8 运行时的类路径、Java 9 运行时的类路径或 Java 9 运行时的模块路径上正确工作。对于模块化多版本 JAR 文件(除了module-info.class其他类之外,还可以针对 Java 9 平台进行编译),情况也是如此。

如果模块描述符未将某些包声明为导出的,因此这些包中的公共类对于该模块来说是私有的,那么当相应的 JAR 文件放置在模块路径上时,这些类将无法访问。但是,如果 JAR 文件放置在类路径中,那么这些类将是可访问的。这是支持类路径和模块路径的不幸后果。

因此,多版本 JAR 文件的公共 API 放置在类路径上与放置在模块路径上时可能会有所不同。通常,如果检测到公共 API 中存在任何观察到的差异,构建多版本 JAR 文件时,jar 工具将尽最大努力失败。但是,在构建模块化多版本 JAR 文件时,如果公共 API 差异是由于将 JAR 文件放置在类路径上时可访问模块私有类而导致的,则建议 jar 工具输出警告。

多版本 jar 和引导加载程序

引导加载程序不支持多版本 JAR(例如,使用 -Xbootclasspath/a 选项声明多版本 JAR 文件时)。对于罕见的用例,这种支持将使引导加载程序的实现变得复杂。

备择方案

一种常见的方法是使用静态反射检查来确定 API 功能是否存在,并相应地选择分别依赖于该功能或不依赖于该功能的适当类。反射成本是在类初始化时产生的,而不是每次使用依赖特性时产生的。选择 Java 平台版本进行编译,并将源标志和目标标志设置为较低版本,以生成与该较低版本兼容的类文件。这种方法通常会通过Animal Sniffer等工具进行增强,以检查 API 不兼容性,除了强制执行 API 兼容性之外,还可以对代码进行注释以说明它是否依赖于更高版本的 Java 平台。这种方法有很多限制:

  1. 反射检查需要仔细维护。

  2. 无法利用较新的语言功能。

  3. 如果删除平台发布 API 功能(可能是内部 API),则相关代码将无法编译。

考虑了“胖”类文件,其中一个类可能具有一个或多个针对不同 Java 平台版本的方法。就支持此类方法声明和动态选择所需的语言和运行时功能而言,这被认为过于复杂。

invokedynamic由于需要保持二进制兼容性,无法使用方法句柄 ( )。

风险和假设

预计 MRJAR 的生产主要与现有流行的构建工具以及支持此类工具的 IDE 兼容,但开发人员的体验可以通过增强功能得到改善。

Maven 使用多模块项目可以支持 MRJAR 文件的源布局和构建。例如,请参阅示例 Maven 项目,它可以生成当前基本的 MRJAR 文件。将有一个用于根和特定 Java 平台版本的子项目,以及一个将上述子项目组装成 MVJAR 的子项目。组装过程可以得到增强,也许可以使用特定的 Maven 插件,利用与该jar工具相同的功能来强制向后兼容。

MRJAR 的运行时处理的设计和实现当前假设运行时使用 URL 类加载器或自定义类加载器JarFile来获取特定于平台的类文件。类加载器用于ZipFile加载类的运行时将无法识别 MRJAR。流行的应用框架和工具,如Jetty、Tomcat、Maven等,都需要进行兼容性检查。

依赖关系

Java 平台模块系统正在考虑的增强型 JAR 文件格式将需要考虑多版本 JAR 元数据。

JEP 247(针对旧平台版本进行编译)支持针对旧版本的平台库进行编译,可以帮助构建工具生成多版本 JAR 文件。