Gradle 6更智能的依赖关系降级的原理与实现

361 阅读6分钟

在处理交叉依赖关系时,最大的挑战之一是如何控制它们的版本。流行的库会作为交叉依赖关系出现在你的依赖关系图中的多个地方。 而且很可能在每个路径上的版本信息都是不同的。

通过多篇博文,你已经了解到Gradle为表达复杂的依赖需求提供了丰富的功能集。 在这篇文章中,我们将讨论为什么在降低依赖版本时语义很重要。 而且你将了解到Gradle 6的严格版本功能,它提供了这种语义信息,有效地给你提供了一个强大而精确的工具来处理这个复杂的问题。

在这篇文章中,我们将再次使用谷歌的Guava作为一个说明性的例子,因为。

  • 它是一个非常流行的库,被超过2万个其他库所使用
  • 它在API的稳定性方面有着复杂的历史,这导致了在大型依赖关系图中有时很难进行升级

在下面的依赖关系视图中,从这个构建扫描中,我们可以看到许多Guava版本都在发挥作用。

image.png

然而,这个构建的依赖性声明非常简单:

dependencies {
    implementation("org.optaplanner:optaplanner-core:7.24.0.Final")
    implementation("com.spotify:folsom:1.5.0")
}

只有两个直接的依赖关系,我们已经有了四个Guava版本之间的冲突。 在这个例子中,它总是会解决到25.0-jre 。这是Gradle考虑到依赖关系图中所有版本的结果,并乐观地选择与约束条件相匹配的最高版本,在我们的例子中是25.0-jre

如果我们将这个Gradle项目与匹配的Maven项目进行比较,我们会得到一些不同的结果。 在Maven中,会使用依赖关系最短路径中的第一个来确定版本。 这意味着,在我们的例子中,Guava版本实际上是顺序依赖的,因为两个库都直接依赖Guava。 如果在Maven POM文件中先声明com.spotify:folsom:1.5.0 ,Guava会被解析为24.1-jre 。但是如果先声明org.optaplanner:optaplanner-core:7.24.0.Final ,Guava会被解析为25.0-jre

如果版本升级是个问题呢?

假设将Guava升级到25.0-jre ,对Folsom来说是个问题,因为它依赖于24.1-jre 中的Files.fileTreeTraverser() API,并25.0 中被删除。

在Gradle 6之前,处理这个问题最经常使用的解决方案是。

不幸的是,这些解决方案都没有给出明确的版本降级理由。 当排除依赖关系时,不清楚你是指你对org.optaplanner:optaplanner-core:7.24.0.Final 的使用不需要Guava,还是只为了它对解决版本的副作用。 如果相反,你选择强制要求依赖关系的特定版本,那么你的库的消费者就无法获得这些信息,他们可能只是你多项目构建中的不同项目,然后就会暴露在你解决的不兼容中。

Maven的情况也很类似:

  • 一个exclude ,同样缺乏语义。
  • 像Gradleforce 一样,使用dependencyManagement 来处理横向依赖关系,并不适用于你的库的消费者。

一个有意义的降级

在Gradle 6中,丰富的版本提供了一个增强的严格版本声明。 这个版本声明有以下语义:

  1. 一个严格的版本有效地优先于由声明严格版本的项目的子图贡献的所有其他依赖版本。
  2. 一个严格版本有效地拒绝所有不兼容的版本。

因此在我们的例子中,我们将把版本声明与依赖性约束结合起来,选择Guava24.1-jre

dependencies {
    constraints {
        implementation("com.google.guava:guava") {
            version {
                strictly("24.1-jre")
            }
            because("Guava 25.0-jre removed APIs used by Folsom")
        }
    }
    implementation("org.optaplanner:optaplanner-core:7.24.0.Final")
    implementation("com.spotify:folsom:1.5.0")
}

为什么语义很重要?

在我们的例子中,简单地结合两个依赖导致了代码的损坏。 我们必须弄清楚在我们自己的库的上下文中,哪个依赖版本的组合是可行的。 然后,我们通过添加一个带有严格版本的依赖约束来记录我们的决定。 如果我们正在构建的库被重复使用,为未来的消费者保留这个决定是很重要的。

在Gradle 6之前的解决方案中,这一信息已经丢失。无论是exclude ,还是force ,都没有给我们的消费者带来足够的信息,而我们的消费者很可能会被这个问题所困扰。

Maven的解决方案也有同样的缺点,有同样的潜在后果。

有了Gradle 6的严格版本定义,你的消费者就会知道你的选择。 如果他们的任何其他依赖关系导致Guava更新,构建就会失败,说明你的库对Guava有严格要求 24.1-jre 。有了because 子句提供的额外信息,这些开发者就会知道这个问题,并已经开始寻找自己的解决方案。 他们可以尊重你的选择,也可以通过定义自己的严格版本来推翻它。

严格版本的最佳实践

由于严格版本的语义,在添加严格版本时应注意以下最佳实践。

  • 对于可重复使用的软件库。
    • 建议在可能的情况下,在版本的strictly 部分使用一个版本范围。这给了库的消费者更多的自由,使他们能够找到一个解决方案,而不必依赖另一个严格的版本定义。
    • 提供一个prefer ,当消费者不关心的时候可以使用这个版本。
  • 对于应用程序,strictly 部分的固定版本是最简单和最直接的选择。
  • 在所有情况下,请确保because 来记录你的决定。

总结

库重用的一个后果是版本冲突不可避免,尤其是对于流行的库。有时有必要为特定的库组合做出明确的版本选择。Gradle 6的严格版本概念允许你做出这种选择,并为消费者保留这种选择。

虽然Maven的解决机制一开始听起来比较简单,但我们表明,随着依赖关系图的增长,它存在语义问题。 对你的库有意义的解决方案在被消费时就失去了所有意义。

另一方面,Gradle通过将语义与版本声明联系起来,有一个一致的解析模型,允许开发者清楚地表达他们做出的选择,并记录他们的推理。

当一个库被其他人消费时,这些选择也是可用的,让消费者有机会更好地尊重,或推翻和记录库开发者的选择。