更智能的依赖降级

在处理传递依赖时,最大的挑战之一是控制它们的版本。流行的库将在您的依赖关系图中的多个地方显示为传递依赖关系。并且很可能每个路径上的版本信息都不同。

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

在这篇文章中,我们将使用 Google 的 Guava 再次 作为示例,因为

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

在下面的依赖关系视图中,取自 这个构建扫描,我们可以看到许多 Guava 版本正在使用中

Extract of build scan showing conflicting Guava versions

但是,此构建的依赖关系声明非常简单

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。如果 com.spotify:folsom:1.5.0 在 Maven POM 文件中首先声明,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 具有相同的语义缺失。
  • 对于传递依赖项(如 Gradle 的 force)使用 dependencyManagement 不适用于你的库的使用者。

有意义的降级

使用 Gradle 6,丰富版本 提供了增强的严格版本声明。这种版本声明具有以下语义:

  1. 严格版本实际上优先于声明严格版本的项目子图贡献的所有其他依赖项版本。
  2. 严格版本实际上拒绝所有不兼容的版本。

因此,在我们的示例中,我们将该版本声明与 依赖项约束 相结合,以选择 Guava 24.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 通过将语义与版本声明相关联,具有一个一致的解析模型,这使开发人员能够清楚地表达他们所做的选择并记录其推理过程。

当其他人使用某个库时,这些选择就会变得可用,让使用者有机会更好地尊重或推翻并记录库开发者所做的选择。

讨论