使用 Gradle 6 避免依赖地狱

目录

简介

对于许多团队来说,依赖地狱是一个大问题。项目及其依赖关系图越大,维护就越困难。现有的依赖管理工具提供的解决方案不足以有效地解决这个问题。

Gradle 6 旨在提供可操作的工具,以帮助处理这些问题,使依赖管理更具可维护性和可靠性。

例如,以下是来自真实项目的匿名化依赖关系图

A large dependency graph

此图中包含 数百个 不同的库。有些是内部库,有些是 OSS 库。这些模块中有一部分每周会发布多个版本。实际上,对于如此规模的图,您无法避免典型的问题,例如

Method not found at runtime

Multiple SLF4J bindings

依赖问题会在构建和测试产品时引起许多问题,并且每天都可能极具挑战性地找出导致回归的原因、项目为何突然无法构建,或者哪个依赖项负责另一个依赖项的升级。

如果您幸运的话,您会得到一个编译时错误,但通常只有在执行测试甚至在生产运行时才会看到问题。在所有这些情况下,错误通常很难追溯到源头,因为它是在构建工具中的依赖项解析成功后出现的。因此,从依赖管理角度来看,一切都是正确的,但实际上并非如此。

造成这种不匹配的原因是,解析依赖项的引擎没有足够的信息来检测问题,并在可能的情况下自动修复问题。为了向引擎提供更多信息,模块需要携带更多元数据。好消息是,这是 Gradle 6 的重点!

Gradle 6 依赖管理介绍 #

Gradle 6 向前迈进了一步,是依赖管理新时代的推动者。借助 Gradle 模块元数据,Gradle 现在支持更丰富、更智能的依赖声明模型,这使构建工具能够做出更好的决策,使构建更可靠,并降低维护依赖关系图的成本。

在依赖管理中看到的许多问题通常是消费者(例如,您构建的应用程序)和生产者(例如,您使用的库/依赖项)之间意见不一致的后果,因为依赖管理引擎没有足够的信息来做出好的决策。

至关重要的是,库(例如 Guava)或框架作者(例如 Spring Boot 或内部框架)可以以更丰富的方式表达需求,以便他们的用户面临更少的依赖管理问题。他们应该能够表达诸如“如果您不知道使用哪个版本,请使用此版本”,或者“如果您使用此功能,那么您还需要这些额外的依赖项”之类的需求。这些是 Gradle 6 提供的众多选项中的一部分。

典型的依赖声明以 groupartifactversion(也称为 GAV 坐标,例如 com.google.guava:guava:25.1)的形式表示。让我们花一点时间关注 version 部分。如果您看到 25.1,它意味着什么?

  • 是您编写代码时最新的发布版本吗?
  • 是您从 StackOverflow 复制粘贴并有效的版本吗?
  • 它是否适用于 25.0
  • 升级到 26.0 可以吗?

与单个版本声明相关的语义缺乏的一个直接后果是,我们很可能执行乐观升级。我们假设因为它适用于 25.1,所以升级到 26.0 应该没问题。实际上,这效果很好,并且这已成为 Gradle 多年来使用的策略。

但是,在某些情况下,乐观升级会失败

  • 主版本升级(破坏二进制兼容性)
  • 漏洞(您真的不应该包含 1.6,因为它有一个 CVE)
  • 回归(1.6 中存在一个错误)
  • 库属于需要共享相同版本的一组更大的模块(例如 Jackson Core、Databind、Annotations 等...)
  • ...

例如,Gradle 6 为您提供了以更丰富的模型表达事物的能力

  • 您需要此依赖项严格在 [1.0, 2.0[ 范围内(因为它遵循语义版本控制)
  • 并且在范围内,您偏好 1.5(因为那是您已经测试过的版本)
  • 并且您拒绝 1.6,因为您知道它有一个直接影响您的错误
dependencies {
    implementation("org.sample:sample") {
        version {
            strictly("[1.0, 2.0[")
            prefer("1.5")
            reject("1.6")
        }
    }
}

这意味着,如果没有其他人关心,引擎将选择 1.5。如果另一个依赖项需要 1.7,我们知道我们可以安全地升级到 1.7。但是,如果另一个依赖项需要 2.1,我们现在可以使构建失败,因为两个模块意见不一致。

此外,生产者可能不知道关于依赖项的信息,因为它在库发布后会发生变化:发现的错误、漏洞、不正确的传递依赖项等... 这些信息可以随时推送到依赖管理引擎,作为额外的输入!

值得注意的是,Gradle 提供的改进不仅适用于消费者。作为库作者,您比以往任何时候都更灵活地表达您的产品:应该对齐版本的不同模块、具有可选功能的库、依赖项版本的建议平台、用于不同运行时版本的不同二进制文件等等!

Gradle 已经在几个版本中提供了这些功能。但是,它们的使用主要限于多项目设置。借助 Gradle 6,所有这些工具现在都可供库作者和消费者使用,方法是在发布的模块中使用 Gradle 模块元数据来支持它们。它能够更清晰地表达需求,并允许引擎计算最佳解决方案。

Gradle 模块元数据 #

由于 Gradle 依赖模型比其他构建工具(Ant+Ivy、Maven、Bazel ...)提供的更丰富,因此我们需要一种元数据格式,以便为发布在 Maven Central、Artifactory 或 Nexus 等二进制仓库上的库启用所有这些功能。这种元数据格式基本上是 Gradle 模型的序列化。您可以在我们的专门的博客文章中了解更多信息。

在 Gradle 6.0 中,默认情况下启用 Gradle 模块元数据的发布。

作为库作者,您不必担心使用 Gradle 特定功能:在所有情况下,仍然可以发布 Maven 或 Ivy 元数据,并且我们尽力将 Gradle 特定概念映射到这些格式(如果可能)。如果不可能,则仅意味着某些功能仅适用于 Gradle 用户,但通常 Maven 用户与今天相比不会失去任何东西。

实践应用 #

最后但并非最不重要的一点是,对于 Gradle 6,我们已经大幅重写了用户指南的 依赖管理文档 部分,使其更以用例为中心。

在接下来的几周内,我们将发布一系列博客文章,更详细地介绍不同的用例。特别是,我们将解释您可以使用 Gradle 6 做什么,包括

Gradle 6 是朝着更好的依赖管理迈出的重要一步,但开发并没有就此止步:我们知道我们还有很多工作要做,我们将解决您的反馈,请随时提出!

讨论