更智能的依赖降级

目录

引言

处理传递性依赖时,最大的挑战之一是控制其版本。流行的库会在依赖图中多个位置显示为传递性依赖。并且,每个路径上的版本信息很可能不同。

通过多篇博客文章,您已经了解到 Gradle 提供了丰富的功能集来表达复杂的依赖需求。在本文中,我们将讨论语义在降级依赖版本时为何重要。您将了解 Gradle 6 的严格版本功能,该功能提供了此语义信息,并有效地为您提供了处理此复杂问题的强大而精确的工具。

在本文中,我们将再次以Google 的 Guava 为例,因为

在下面的依赖视图中,从此构建扫描中可以看到,有许多 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。如果在 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 的`exclude`
  • 将 Guava 的 `resolutionStrategy.force` 添加到版本 `24.1-jre`

不幸的是,这些解决方案都没有为版本降级提供清晰的理由。在排除依赖项时,不清楚您是表示您对 `org.optaplanner:optaplanner-core:7.24.0.Final` 的使用不需要 Guava,还是仅仅因为其对解析版本有副作用。相反,如果您选择强制使用特定版本的依赖项,则该信息将无法提供给您的库的消费者,这可能是您多项目构建中的另一个项目,它将面临您已解决的相同不兼容问题。

Maven 的情况非常相似

  • `exclude` 具有相同的语义缺失。
  • 对于传递性依赖,使用 `dependencyManagement`,就像 Gradle 的 `force` 一样,不适用于您库的消费者。

有意义的降级 #

通过 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 通过将语义与版本声明相关联,具有一致的解析模型,这使得开发人员能够清晰地表达他们所做的选择并记录他们的推理。

当一个库被其他库消费时,这些选择将可用,这为消费者提供了更好地遵守,或推翻并记录库开发人员所做选择的机会。

讨论