多版本JAR-好不好?


从一个翻译者那里:我们正在积极地将该平台翻译到Java 11轨道上,并在考虑如何利用Java 8/11的功能来考虑如何有效地开发Java库(例如YARG ),从而不必创建单独的分支和发行版。 一种可能的解决方案是多版本JAR,但并非一切都顺利。


Java 9包括一个称为多发行版JAR的新Java运行时选项。 这也许是平台上最具争议的创新之一。 TL; DR:我们发现这是一个解决严重问题的歪曲方案 。 在本文中,我们将解释为什么会这样,并告诉您如何构建这样的JAR。


多重发行JAR或MR JAR是JDK 9中引入的Java平台的新功能。在这里,我们将详细描述与使用该技术相关的重大风险,以及如何使用Gradle创建多重发行JAR,如果您仍然想要。


实际上,多发行版JAR是Java归档文件,其中包括同一类的多个变体,用于处理不同版本的运行时。 例如,如果您使用的是JDK 8,则Java环境将使用JDK 8的类版本;如果使用的是Java 9,则将使用Java 9的版本;类似地,如果该版本是为将来的Java 10版本创建的,则运行时将使用此版本而不是Java的版本。 9或默认版本(Java 8)。


在猫的帮助下,我们了解了新JAR格式的设备,并确定了这一切。


何时使用多版本JAR


  • 优化的运行时间。 这是许多开发人员面临的问题的解决方案:开发应用程序时,尚不清楚它将在哪个环境中执行。 但是,对于某些版本的运行时,您可以嵌入同一类的通用版本。 假设您要显示运行应用程序的Java的版本号。 对于Java 9,可以使用Runtime.getVersion方法。 但是,这是仅在Java 9+中可用的新方法。 如果需要其他运行时,请说Java 8,则必须解析java.version属性。 结果,您将对一个函数有2种不同的实现。
  • API冲突:API之间的冲突解决也是一个常见问题。 例如,您需要支持两个运行时,但其中之一不赞成使用API​​。 有两种常见的解决方案:


    1. 首先是反思。 例如,您可以指定VersionProvider接口,然后指定2个特定的类Java8VersionProvider和Java9VersionProvider,并将相应的类加载到运行时(有趣的是,为了在两个类之间进行选择,您必须解析版本号!)。 此解决方案的一种选择是使用各种使用反射方法调用的方法来创建单个类。
    2. 一个更高级的解决方案是在可能的地方使用方法句柄 。 反射很可能会给您带来抑制性和不适感,并且总的来说是这样。


多发行版JAR的已知替代品


第二种方法(更简单易懂)是为不同的运行时创建2个不同的归档。 从理论上讲,您在IDE中创建了同一类的两个实现,并且编译,测试并将它们正确打包在2个不同的工件中是构建系统的任务。 这种方法已经在Guava或Spock中使用了很多年。 但是对于像Scala这样的语言,它也是必需的。 所有这些都是因为编译器和运行时有太多选择,以至于二进制兼容性几乎变得无法实现。


但是还有许多其他原因需要使用单独的JAR归档文件:


  • JAR只是打包的一种方式。

它是一个包含类的程序集工件,但这还不是全部:通常,资源也包含在存档中。 与资源处理一样,包装也要付出代价。 Gradle团队旨在提高构建质量并减少开发人员通常编译结果,测试和构建过程的等待时间。 如果归档文件过早出现在流程中,则会创建不必要的同步点。 例如,要编译依赖于API的类,唯一需要的是.class文件。 不需要jar档案或jar中的资源。 同样,只需要成绩文件和资源即可运行Gradle测试。 对于测试,无需创建一个jar。 只有外部用户(即发布时)才需要它。 但是,如果必须强制创建工件,那么某些任务将无法并行执行,并且整个组装过程都将受阻。 如果对于小型项目而言,这并不重要,那么对于大型公司项目而言,这是主要的减速因素。


  • 更重要的是,作为工件的jar归档文件不能携带有关依赖项的信息。

Java 9和Java 8运行时中每个类的依赖关系不太可能相同。 是的,在我们的简单示例中将是这样,但事实并非如此:通常,用户导入用于Java 9功能的库的反向端口,并使用它来实现Java 8类的版本。会有一些具有不同依赖树的元素。 这意味着,如果您使用的是Java 9,则将拥有不再需要的依赖项。 而且,它污染了类路径,给库用户造成了可能的冲突。


最后,在一个项目中,您可以创建用于不同目的的JAR:


  • 用于API
  • 对于Java 8
  • 对于Java 9
  • 具有本地绑定

依赖性分类器的错误使用会导致与共享同一机制相关的冲突。 通常, javadocs是作为分类器安装的,但实际上它们没有依赖性。


  • 我们不想产生不一致;构建过程不应取决于您如何获取类。 换句话说,使用多发行版jar有一个副作用:现在,从JAR存档进行调用和从类目录进行调用是完全不同的事情。 它们在语义上有很大的不同!
  • 根据用于创建JAR的工具的不同,最终可能会得到不兼容的JAR归档文件! 唯一保证在将两个类选项打包到一个归档文件中时,它们将具有一个开放的API的工具-jar实用程序本身。 这并非没有原因,不一定涉及组装工具甚至用户。 JAR本质上是一个类似于ZIP存档的“信封”。 因此,根据收集方式的不同,您会得到不同的行为,或者可能会收集到错误的工件(您不会注意到)。

管理单个JAR存档的更有效方法


开发人员不使用单独的档案的主要原因是,它们不便于收集和使用。 应该指责构建工具,这在Gradle之前根本没有解决。 特别是,那些在Maven中使用此方法的人只能依靠弱分类器功能来发布其他工件。 但是, 分类器在这种困难的情况下无济于事。 它们用于各种目的,从发布源代码,文档,javadocs到实现库选项(guava-jdk5,guava-jdk7等)或各种用例(api,fat jar等)。 在实践中,没有办法表明分类器依赖树与主项目的依赖树不同。 换句话说,POM格式从根本上被破坏了,因为 它表示组件的组装方式和提供的工件。 假设您需要实现2个不同的jar归档文件:classic和fat jar,其中包括所有依赖项。 Maven决定2个工件都具有相同的依赖树,即使这显然是错误的! 在这种情况下,这显然不言而喻,但情况与多发行版JAR相同!


解决方案是正确处理选项。 Gradle可以通过基于选项管理依赖项来做到这一点。 该功能目前可在Android上进行开发,但我们也在开发适用于Java和本机应用程序的版本!


基于变体的依赖性管理基于以下事实:模块和工件是完全不同的事物。 考虑到不同的要求,相同的代码可以在不同的运行时完美运行。 对于那些使用本机编译的人来说,很长一段时间以来就很明显了:我们为i386amd64进行编译,而决不能通过arm64干扰i386库的依赖! 在Java上下文中,这意味着对于Java 8,您需要创建“ java 8” JAR归档文件的版本,其类格式将对应于Java8。此工件将包含元数据,以及有关使用哪些依赖项的信息。 对于Java 8或9,将选择与版本相对应的依赖项。 就是这么简单(实际上,原因不是运行时只是选项的一个字段,您可以组合多个字段)。


当然,由于过分的复杂性,以前没有人做过:显然,Maven绝不允许执行这种复杂的操作。 但是有了Gradle,这是可能的。 Gradle团队目前正在研究一种新的元数据格式,该格式告诉用户要使用的选项。 简而言之,构建工具必须处理此类模块的编译,测试,打包和处理。 例如,该项目应在Java 8和Java 9运行时中运行,理想情况下,您需要实现2个版本的库。 这意味着有2个不同的编译器(在Java 8中工作时避免使用Java 9 API),2个类目录以及最终2个不同的JAR归档文件。 而且,很有可能需要测试2个运行时。 或者,您实现2个归档,但是决定在Java 9运行时中测试Java 8版本的行为(因为这可能在启动时发生!)。


该方案尚未实施,但Gradle团队在此方向上已取得重大进展


如何使用Gradle创建多发行版JAR


但是,如果此功能尚未就绪,该怎么办? 放松,以相同的方式创建正确的工件。 在Java生态系统中出现上述功能之前,有两种选择:


  • 使用反射或其他JAR存档的好方法;
  • 使用多版本的JAR(请注意,即使有很好的用例,这也可能是一个不好的解决方案)。

无论选择什么,使用不同的归档文件或多版本JAR,方案都是相同的。 本质上,多发行版JAR的包装是错误的:它们应该是一个选择,但不是目标。 从技术上讲,单个和外部JAR的源布局都是相同的。 该存储库描述了如何使用Gradle创建多发行版JAR。 该过程的实质在下面简要描述。


首先,您应该始终记住开发人员的一个坏习惯:他们使用计划在其上发布工件的Java版本运行Gradle(或Maven)。 此外,有时会使用更高版本来启动Gradle,并且会在更早的API级别上进行编译。 但是没有特别的理由。 在Gradle中,可以进行Ross编译 。 它允许您描述JDK的位置,以及将编译作为一个单独的过程运行,以使用此JDK编译组件。 配置各种JDK的最佳方法是通过环境变量配置JDK的路径,如本文件所述 。 然后,您只需根据与源/目标平台的兼容性,将 Gradle配置为使用正确的JDK。 值得注意的是,从JDK 9开始,交叉编译不需要JDK的早期版本。 这是一个新功能-release。 Gradle 使用此功能,并将根据需要配置编译器


另一个关键点是指定来源集 。 源集是需要一起编译的一组源文件。 JAR是通过编译一个或多个源集获得的。 对于每个集合,Gradle自动创建一个适当的自定义编译任务。 这意味着,如果有Java 8和Java 9的源,则这些源将位于不同的集合中。 这正是Java 9的源集中的工作方式 ,其中将包含我们类的一个版本。 这确实有效,并且您不需要像在Maven中那样创建单独的项目。 但最重要的是,此方法使您可以微调集合的编译。


具有一个类的不同版本的困难之一是,类代码很少独立于其余代码(它与不在主集中的类具有依赖性)。 例如,它的API可以使用不需要特殊源的类来支持Java9。同时,我不想重新编译所有这些公共类并将其版本打包为Java9。它们是公共类,因此它们必须与特定JDK的类。 我们在这里进行设置:在Java 9的源集和主集之间添加一个依赖项,以便在编译Java 9的版本时,所有公共类都保留在编译类路径中。


下一步很简单 :您需要向Gradle解释主要的源集将使用Java 8 API级别进行编译,而用于Java 9的源将使用Java 9级别进行编译。


以上所有内容将帮助您使用前面提到的两种方法:实现单独的JAR归档文件或多版本JAR。 既然帖子是关于这个主题的,那么让我们看一个如何使Gradle构建一个多发行版JAR的示例:


jar { into('META-INF/versions/9') { from sourceSets.java9.output } manifest.attributes( 'Multi-Release': 'true' ) } 

此块描述:将Java 9的类打包在META-INF /版本/ 9目录中(用于MR JAR),并在清单中设置多版本标签。


就是这样,您的第一个MR JAR已准备好!


但是,不幸的是,有关此工作尚未结束。 如果您使用Gradle,您将知道使用应用程序插件时,可以直接通过运行任务运行应用程序。 但是,由于Gradle通常会尽量减少工作量,因此运行任务必须同时使用类目录和已处理资源的目录。 对于多发行版JAR,这是一个问题,因为立即需要JAR! 因此,不必使用插件,而必须创建自己的task ,这是反对使用多发行版JAR的一个论点。


最后但并非最不重要的一点是,我们提到我们需要测试该类的2个版本。 为此,您不能在单独的进程中使用VM,因为Java运行时没有等效的-release标记。 这个想法是只需要编写一个测试,但是它将执行两次:在Java 8和Java 9中。这是确保特定于运行时的类正常工作的唯一方法。 默认情况下,Gradle创建一个测试任务,并且以相同的方式使用类目录而不是JAR。 因此,我们将做两件事:为Java 9创建一个测试任务,并配置这两个任务,以便它们使用JAR和指定的Java运行时。 该实现将如下所示:


 test { dependsOn jar def jdkHome = System.getenv("JAVA_8") classpath = files(jar.archivePath, classpath) - sourceSets.main.output executable = file("$jdkHome/bin/java") doFirst { println "$name runs test using JDK 8" } } task testJava9(type: Test) { dependsOn jar def jdkHome = System.getenv("JAVA_9") classpath = files(jar.archivePath, classpath) - sourceSets.main.output executable = file("$jdkHome/bin/java") doFirst { println classpath.asPath println "$name runs test using JDK 9" } } check.dependsOn(testJava9) 

现在,当任务开始时, 选中 Gradle将使用所需的JDK编译每组源代码,创建一个多版本的JAR,然后在两个JDK上使用此JAR运行测试。 Gradle的未来版本将更加声明性地帮助您。


结论


总结一下。 您了解到,多发行版JAR是解决许多库开发人员面临的实际问题的尝试。 但是,此问题的解决方案似乎不正确。 正确的依赖关系管理,链接工件和选项,关心性能(能够并行执行尽可能多的任务的能力)-所有这些使MR JAR成为穷人的解决方案。 使用选项可以正确解决此问题。 但是,尽管正在开发使用Gradle中的选项进行的依赖关系管理,但在简单情况下,多发行版JAR还是很方便的。 在这种情况下,这篇文章将帮助您了解如何执行此操作,以及Gradle的哲学与Maven(源代码集与项目)有何不同。


最后,我们不否认在某些情况下可以使用多版本JAR:例如,当不知道将在哪个环境中执行应用程序(不是库)时,这是一个例外。 在这篇文章中,我们描述了库开发人员面临的主要问题,以及多版本JAR如何解决这些问题。 与多发行版JAR相比,通过选项进行正确的依赖关系建模可以提高性能(通过细粒度的并行性)并降低维护成本(避免不可预见的复杂性)。 在您的情况下,可能还需要MR JAR,因此Gradle已经解决了这一问题。 看一下这个示例项目 ,然后自己尝试。

Source: https://habr.com/ru/post/zh-CN428868/


All Articles