多版本

JEP-238引入了对多版本 jar 的支持。这意味着您可以在一个 jar 中包含 Java 版本相关的类。根据运行时,它将选择一个类的最佳匹配版本。

JEP-238 简介

“前向兼容性”问题

这个 JEP 试图解决的问题是即使代码库必须与早期版本保持兼容,也可以使用新的 JDK 功能。

让我们试着用一个真实的例子来具体化:在 Java 7中添加了java.nio,它在文件处理方面要好得多。在那些日子里,Maven 仍然需要 Java 6 才能运行,但是当 Maven 在 Java 7 或更高版本上运行时,他们想利用这些新特性。

在 Java 8 之前,有两种解决方案:

  1. 使用目标 JDK(即 Java 6)编译并使用反射。您可以确保代码与 Java 6 兼容,但反射部分很难验证。
      if ( isAtLeastJava7() ) {
        Method toPathMethod = f.getClass().getMethod( "toPath" );
        Object toPathInstance = toPathMethod.invoke( f );
        Method isSymbolicLink = Class.forName( "java.nio.file.Files" ).getMethod( "isSymbolicLink" );
        ...
      }
      else {
        // compare absoluteFile with canonicalFile 
        ...
      }
  2. 使用所需的 JDK(即 Java 7)进行编译,但使用最低版本(即 1.6)的源/目标。这里的危险是您无法确保所有代码都与 Java 6 兼容。根据代码结构,Animal Sniffer可能会有所帮助。
      if ( isAtLeastJava7() ) {
        return Files.isSymbolicLink( f.toPath() );
      }
      else {
        // compare absoluteFile with canonicalFile 
        ...
      }

“前向兼容”解决方案

A.class
B.class
C.class
D.class
META-INF/MANIFEST.MF { Multi-Release: true }
         versions/9/A.class
                    B.class
                  10/A.class
                     C.class
                      

使用 MANIFEST 文件中的Multi-Release: true标志,Java 运行时还将在META-INF/versions中查找特定于版本的类,否则仅使用基类。

挑战

多版本 jar 背后的理论非常简单,但在实践中它可能变得非常复杂。您必须确保所有课程保持同步;如果您将一个方法添加到一个类,不要忘记将它添加到其他类。有一些选项可以减少这个级别的问题:让所有多版本类实现一个接口,并在基码中实例化该类,但只调用接口方法。最好的方法是使用所有目标 Java 版本测试 *jar*。在将 jar 变成多版本 jar 之前,您应该三思而后行,因为这样的 jar 可能难以阅读、维护和测试。通常应用程序不需要这个,除非它是一个广泛分布的应用程序并且您不控制目标 Java 运行时。图书馆应根据以下因素做出决定:我需要这个新的 Java 功能吗?我可以将此 Java 版本作为新要求吗?使用第一段中提到的 else/if 语句来解决这个问题是否可以接受?

在创建 Multi Release jar 时,应该知道几个重要的事实。

  • 必须为每个不同的版本调用 Java 编译器。解决方案通过拥有多个 Maven 项目/模块或通过向 POM 添加额外的编译器执行块(如带有 module-info 的旧项目)来解决这个问题。
  • Multi-Release: true属性仅在类位于 jar 中时才被识别。换句话说,您无法测试放在target/classes/META-INF/versions/${release}/中的类。
  • 在编写所有 IDE 之前,每个 Maven 项目只能有一个 JDK,而对于多版本,您希望为每个源文件夹指定它。

模式 1:Maven 多模块

这是 Maven 团队自己提供的第一个模式。他们有以下要求:

  • 只需一次 Maven 调用即可编译、测试和打包 Multi Release jar
  • 它必须与 IDE 一起使用
  • 开发者不应该改变他们的工作方式/简单的配置

涵盖前两个项目符号的唯一解决方案是将代码拆分为 Maven 多模块项目。现在每个 Maven 模块只是一个标准的 Maven 项目,在 pom.xml 中几乎没有特定的调整。您可以通过多种方式运行此项目:

  • 使用所需的最高 JDK 版本来构建项目。您可以使用release来确保代码仅使用匹配的代码和语法。由于版本代码是隔离的,因此您可以使用更高的 Java 运行时运行 surefire。
  • 如果您真的想使用匹配的 Java 版本进行编译和测试,请使用工具链。

即使结果只是 1 个工件,它也需要分层结构的缺点。

模式 2:多项目

此解决方案是对先前 Maven 多模块设置的响应。要求几乎一样

  • 不要求项目切换到多模块格式
  • 开发者不应该改变他们的工作方式/简单的配置

第一个要求意味着这些项目现在是单独的 Maven 项目。一个 Maven 项目包含基本代码,最终这也将是多版本 jar。第一次构建此类项目时,您需要调用 Maven 至少 3 次:首先需要编译基础,接下来必须构建所有版本特定的项目,最后需要再次构建主项目,现在包括从 jar 中提取的特定于版本的类。此设置很紧凑,但具有循环依赖性。这需要一些技巧,并使发布变得更加复杂。另一个缺点是您必须将 SNAPSHOT 安装到本地存储库,并且在发布时它需要基础项目的 2 个版本,一个为 multirelease-nine 做准备,一个为已发布的 multirelease-nine 做准备。

模式 3:单个项目

到目前为止,有 3 个解决方案,每个解决方案都受到其先前版本的启发。

主要目标:

  • 不要求项目切换到多模块格式
  • 只需一次 Maven 调用即可编译和打包 Multi Release jar

单个项目

在这种情况下,一切都保留在一个 Maven 项目中。每个特定的 Java 版本都有自己的源文件夹和输出文件夹,并且在打包之前将它们组合在一起。没有涵盖的是如何测试每个类。

多发布父级

这种方法用 maven-compiler-plugin 中的额外执行块替换了 maven-ant-plugin。它已设置为父级,因此其他项目可以使用它。它使用工具链来构建具有匹配 Java 版本的所有类,因此您始终可以获得多版本 jar。由于巨大的配置,而且由于 Maven 还不支持 mixin,所以将它全部放在父级中是有意义的。然而,同时surefire只被调用一次。

CI服务器

这种方法通过只为特定 Java 版本的源指定执行块来减少以前的解决方案。它不使用工具链,而是使用 JDK 来运行 Maven。这意味着只有特定 Java 版本的源代码才会被编译和测试。该解决方案严重依赖于每个目标 Java 版本都可用的 CI 服务器。如果 CI 服务器成功,则所有类都使用其匹配的 Java 版本进行测试。

模式四:Maven扩展+插件

这种方法引入了一种新的打包类型,并且一个额外的插件负责 maven-compiler-plugin 的多次执行,但这些现在由multi -release-jar-maven-plugin的perReleaseConfiguration处理。没有涵盖的是如何测试每个类。

模式总结

对于每个模式,都会基于相同的源文件集创建集成测试。见https://github.com/apache/maven-compiler-plugin/tree/master/src/it/multirelease-patterns

Maven 多模块 多项目 单个项目(运行时) 单个项目(工具链) Maven扩展+插件
# 项目 1 1 + #java版本 1 1 1
# 构建打包 1 2 + #java版本 1 1 1
# 构建/项目来测试 1 1 #java版本 1 不适用(一)
简单的 Maven 项目布局 是的 是的 是的 是的
额外的 POM 调整(b) 1(c) #java版本(d) #javaVersion(e) ??(F) #java版本(g)
包含模块描述符 否 (h) 否 (h) 是的 是的 是的
IDE 支持 (i) 是的 是的

(a) 项目只能使用最高要求的 JDK 执行,因此您不能测试所有 JDK 的代码

(b) 额外的 POM 调整:添加到默认生命周期的执行次数。这反映了 POM 的复杂性。

(c) maven multimodule 使用maven-assembly-plugin 组装成multirelease jar

(d) 多项目使用 maven-dependency-plugin 将 java 特定依赖解包到其匹配的 outputDirectory

(e) 每个所需的 Java 版本都有一个配置文件,其中包含该 Java 版本的额外执行块。

(F)

(g) Maven扩展+插件隐藏perReleaseConfiguration配置中的多次执行

(h) 在依赖项上需要一个 --patch-module

(i) IDE 支持:所有类都被识别并且可以在 IDE 中进行测试。