每日必读DZone News—多版本JAR:好的还是坏的想法?

每日坚持必读,就是紧随时代发展的步伐,技术之路虽艰辛,但终会有所收获。每天进步一小步,程序的世界已然不同。Java Zone成就每个程序员的不同。英文原文地址:https://dzone.com/articles/multi-release-jars-good-or-bad-idea?edition=347127&utm_source=Daily%20Digest&utm_medium=email&utm_campaign=Daily%20Digest%202017-12-27

Gradle的人认为,Java 9的多版本JAR对于图书馆设计人员来说是一个糟糕的解决方案。 看看如何创建一个MRJAR,为什么你不应该这样做。

· Java Zone

Java 9带来了Java运行时的新功能,称为多版本JAR。对于我们Gradle来说,这可能是平台上最有争议的补充之一。 TL / DR,我们认为这是一个真正的问题的错误答案。这篇文章将解释为什么我们这么认为,但也解释如何你可以建立这样的JAR,如果你真的想。

多版本JAR,又名MRJAR,是Java 9 JDK中包含的Java平台的一项新功能。在这篇文章中,我们将详细说明采用这种技术的重大风险,并提供如何使用Gradle生成和使用带有Gradle的多版本JAR。

简而言之,多版本JAR允许您打包同一类的多个版本,供不同运行时间使用。例如,如果您在JDK 8上运行,则Java运行时将使用该类的Java 8版本,但是如果您在Java 9上运行,则将使用特定于Java-9的实现。同样,如果为即将发布的Java 10版本构建版本,则运行时将使用该版本,而不是Java 9和默认(Java 8)版本。


多版本JAR的用例


优化的运行时间:

这个问题解决了很多开发人员在现实世界中遇到的问题:开发应用程序时,您不知道将在什么运行时间执行。但是,您知道对于某些运行时,您可以实现相同类的优化版本。例如,假设您想要显示您的应用程序当前正在执行的Java版本号。对于Java 9,可以使用Runtime.getVersion方法。但是,这是一个新的方法,只有在Java 9 +上运行才可用。如果您定位更多运行时,请说Java 8,那么您需要解析java.version属性。所以你最终有两个不同的实现相同的功能。

冲突的API:

另一个常见的用例是处理冲突的API。例如,您需要支持两个不同的运行时,但有一个已经弃用了API。目前有两个广泛使用的解决方案来解决这个问题:

  •         第一个是使用反射。例如,可以定义一个VersionProvider接口,然后定义两个具体的类Java8VersionProvider和Java9VersionProvider,它们是在运行时加载的(注意,有趣的是,为了能够在两者之间进行选择,您可能需要解析版本​​号! )。这个解决方案的一个变体是只有一个类,但是不同的方法,通过反射访问和调用不同的方法。
  •         如果技术上适用,更高级的解决方案是使用方法手柄。最有可能的是,你会认为反思既是痛苦的,也是缓慢的,你很可能是对的。

众所周知的多版本JAR的替代方案


另一个更易于维护和推理的解决方案是提供两种不同的JAR,针对两种不同的运行时间。 基本上,您可以在IDE中为同一个类编写两个实现,而编译工具负责编译,测试并将它们正确打包为两个不同的工件。

这是一些像Guava或Spock这样的工具已经使用多年的方法。 但这也是Scala需要的一些语言。 因为编译器和运行时有太多的变种,所以二进制兼容性几乎不可能维护。

但有更多的理由更喜欢单独的JAR:

JAR只是包装:
        
这是构建包的类的构件的工件,但不仅如此:资源通常也会捆绑到JAR中。包装(以及加工资源)都有成本。我们正在尝试使用Gradle来提高构建的性能,并减少开发人员等待查看编译,测试结果以及整个构建过程的时间。

        
在这个过程中,通过强制构建JAR,您会创建一个冗余的同步点。例如,要编译下游消费者,消费者需要的唯一东西就是.class文件。它不需要JAR,也不需要JAR中的资源。

        
同样,为了执行测试,所有的Gradle需求都是类文件,加上资源。没有必要真正创建JAR来执行测试。 JAR只有在外部消费者需要时才需要(简而言之,是发布)。但是,只要你认为这个神器是一个需求,那么你就阻止了一些同时运行的任务,而你正在放慢整个构建。而对于小项目来说,这可能不是问题。对于企业规模的构建来说,这是一个主要的障碍。
更重要的是,作为一个工件,JAR不应该携带关于依赖的信息:
        
Java 9特定类的运行时依赖关系与Java 8特定类的运行时依赖关系是完全一样的。在我们这个非常简单的例子中,他们会这样做,但是对于一个更大的项目来说,这是不好的建模:通常,用户将导入一个Java 9特性的bac​​kport库,并用它来实现该类的Java 8版本。

        
但是,如果将两个版本都打包在同一个JAR中,那么您将不具有相同依赖性树的东西混合到一个工件中。这意味着,通常情况下,如果你碰巧在Java 9上运行,那么就会带来一个永远不会使用的依赖关系。更糟的是,它可能(也会)污染你的班级路线,可能会给消费者造成冲突。


最终,对于单个项目,您可以生成针对不同用途的不同JAR,例如:

  •   一个用于API
  •   一个用于Java 8运行时   
  •  一个用于Java 9
  •   一个具有本地绑定

分类器的滥用导致使用相同的机制引用不一致的事物。通常情况下,源代码或javadocs JAR将作为分类器发布,但实际上并没有任何依赖关系。

  •     我们不想创建一个不匹配,取决于你如何得到你的类。换句话说,使用多版本JAR具有从JAR中消耗和从类目录中消耗的副作用不再等价。两者之间有语义上的差别,这太可怕了!
  •     根据要创建JAR的工具,您可能会产生不一致的JAR!到目前为止,唯一的工具可以保证,如果你在一个JAR中打包两个相同的类,它们都具有相同的公共API,那就是jar工具本身 - 由于很多好的原因,它不一定被构建工具使用,甚至用户。实际上,JAR只是一个信封。这是一个伪装的ZIP。所以取决于你如何构建它,你会有不同的行为,或者你可能会产生错误的文物,永远不会注意到。


更好的方法来管理单独的JAR


开发人员不使用单独的JAR的主要原因是生产和消费不切实际。这个错误在构建工具上,直到Gradle,在处理这个问题上已经失败了。

特别是,使用这个解决方案的开发人员除了依靠Maven的非常差的分类器功能来发布其他工件以外别无选择。但是,分类器在模拟情况的复杂性方面非常糟糕。它们被用于各种不同的方面,从出版源,文档,javadocs到发布库(guava-jdk5,番石榴-jdk7,...)或不同的用法(API,fat JAR,...)的变体。

实际上,没有办法指出分类器的依赖关系树不是项目本身的依赖关系树。换句话说,POM被破坏了,因为它代表了组件是如何构建的以及它产生了什么工件。假设你想生成两个不同的JAR:一个经典的JAR和一个捆绑所有依赖的胖子jar。

在实践中,Maven会认为这两个文物具有相同的依赖树,即使它是错的!在这种情况下,这是非常明显的,但情况与多版本JAR完全相同!

解决方案是正确处理变体。这就是我们所说的变体感知依赖管理,Gradle知道如何去做。到目前为止,这个功能只能用于Android开发,但是我们目前正在为Java和本地开发它。

支持变体的依赖管理就是模块和工件是不同的动物。使用相同的源文件,您可以针对不同的运行时,具有不同的要求。对于本土世界来说,这已经很明显多年了:我们为i386和amd64编译,而且没有办法将i386库的依赖和arm64混合在一起!

转换到Java世界,这意味着如果你的目标是Java 8,你应该生成一个Java 8版本的JAR,其目标类是Java 8类格式。这个工件会附加元数据,以便Java 8消费者知道要使用哪些依赖关系。如果你的目标是Java 9,那么Java 9的依赖关系将被选中。就这么简单(实际上并不是因为运行时只是变量的一个维度,而是可以组合多个变量)。

当然,以前从来没有人做过,因为处理起来很复杂:Maven肯定不会让你做这么复杂的事情。但是Gradle使它成为可能。好消息是,我们还在开发一种新的元数据格式,让消费者知道应该使用哪种变体。

简单地说,构建工具需要处理编译,测试和打包的复杂性,而且还要消耗这些模块。

例如,假设你想支持Java 8和Java 9作为运行时。那么,理想情况下,你需要编译你的库的两个版本。这意味着两个不同的编译器(为了避免在面向Java 8时使用Java 9 API),两个不同的类目录以及两个不同的JAR。

而且,你也许会想测试两个不同的运行时间。或者,您可能需要构建两个JAR,但仍想测试在Java 9运行时执行Java 8版本的行为(因为它可能发生在生产中!)。

我们已经在建模方面取得了重大进展,即使我们还没有准备好,这也解释了为什么我们不那么热衷于使用多版本的JAR:在解决问题的时候,他们正在修复这个问题, Maven Central将会因为没有正确声明其依赖关系的图书馆而臃肿起来!


如何使用Gradle创建多版本JAR


还没有准备好,那我该怎么办? 好消息是生成正确的文物的路径是一样的。 在为Java生态系统准备好这个新功能之前,您有两个不同的选择:

  •      用旧的方法,使用反射或不同的JAR。
  •      使用多版本的JAR,(即使有很好的用例,也可能会在这里做出错误的决定)

无论您选择哪种解决方案,都使用相同的设置。多版本JAR只是错误的(默认)包装:它们应该是一个选项,而不是一个目标。从技术上讲,对于单独的JAR和外部JAR,源布局是相同的。这个存储库解释了如何使用Gradle创建一个多版本的JAR,但是这里简单介绍一下它的工作原理。

首先,你必须明白,我们作为开发人员经常有一个非常坏的习惯:我们倾向于使用与你想要产生的工件相同的Java版本来运行Gradle(或者Maven)。有时甚至更糟 - 当我们使用更新的版本来运行Gradle并使用较旧的API级别进行编译时。

但没有理由这样做。

Gradle支持交叉编译。它可以让你解释在哪里找到JDK和fork编译来使用这个特定的JDK来编译一个组件。设置不同JDK的合理方法是通过环境变量来配置JDK的路径,这就是我们在这个文件中所做的。那么我们只需要配置Gradle来使用基于源/目标兼容性的相应的JDK。值得注意的是,从JDK 9开始,不再需要提供较老的JDK来执行交叉编译。一个新的选项,即释放,就是这样做的。 Gradle会识别这个选项并相应地配置编译器。

第二个关键概念是源集的概念。一个源集合表示将被一起编译的一组源代码。 JAR是根据一个或多个源集的编译结果构建的。对于每个源组,Gradle将自动创建一个可以配置的相应编译任务。这意味着如果我们有Java 8的源代码和Java 9的源代码,那么他们应该生活在不同的源代码集合中。这就是我们通过创建一个包含我们类的专用版本的Java 9特定源代码集所做的。这符合现实,并不强迫你创建一个像Maven一样的单独项目。但更重要的是,它允许我们精确地配置这个源文件将被编译的方式。

部分单个类的多个版本的挑战是这样一个类完全独立于其他代码(它具有对主源集中的类的依赖)是非常罕见的。例如,其API将使用不需要具有Java-9特定源的类。但是,您不想重新编译所有这些常见的类,也不想要将所有这些类的Java 9版本打包在一起。他们真的分享,应该保持分开。这就是这一行的意思:它将配置Java 9源集和主源集之间的依赖关系,确保在我们编译Java 9特定版本时,所有常见的类都在编译类路径上。

接下来的步骤非常简单:我们需要向Gradle解释,主要的源代码集将针对Java 8语言级别,并且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特定的类绑定到MRJar中预期的META-INF / versions / 9目录中,并将多发布标志添加到清单中。

就是这样,你已经建立了你的第一个MRJar!但是,不幸的是,我们还没有完成。如果您熟悉Gradle,您将会知道,如果您应用了应用程序插件,则还可以直接使用运行任务运行应用程序。

但是,因为像往常一样,Gradle尝试执行最少量的工作来完成您所需要的工作,所以运行任务将使用类目录以及已处理的资源目录。而对于多版本的JAR,这是个问题,因为你现在需要JAR!所以不要依赖这个插件,我们别无选择,只能创建自己的任务,这是为什么不使用多版本JAR的另一个原因。

最后但并非最不重要,我们说我们可能也想测试我们班的两个版本。为此,您别无选择,只能使用分叉虚拟机,因为没有与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解决了大量图书馆设计师面临的实际问题。但是,我们认为这是解决问题的错误。正确的依赖性建模,以及工件和变体的耦合,以及不要忘记性能(同时执行更多任务的能力)使他们成为一个穷人的解决方案,我们正在使用变体感知依赖管理解决问题。

但是,我们认为,对于简单的使用情况,知道用于Java的变体感知依赖性管理还没有完成,那么生成这样的JAR可能是方便的。在这种情况下,只有在这种情况下,这篇文章才能帮助你理解你如何做到这一点,以及Gradle的哲学与Maven的不同之处(源文件集还是项目文件)。

最后,我们并不否认有多个版本的JAR是有意义的:例如运行时未知的应用程序,但是这些应用程序是例外的,应该这样考虑。

大多数问题都是针对图书馆设计人员的:我们已经介绍了他们面临的常见问题,以及多版本JAR如何尝试解决其中的一些问题。通过使用多版本JAR,变体可以正确建模依赖关系,从而提高性能(通过更细粒度的并行),并减少维护开销(避免意外的复杂性)。你的情况可能决定使用MRJARs;放心,它仍然支持Gradle。看到这个mrjar gradle示例项目今天尝试这个。



你可能感兴趣的:(DZone每日必读)