使用 Gradle 6 避免依赖地狱
简介
对于许多团队来说,依赖地狱是一个大问题。项目及其依赖关系图越大,维护就越困难。现有的依赖管理工具提供的解决方案不足以有效地解决这个问题。
Gradle 6 旨在提供可操作的工具,以帮助处理这些问题,使依赖管理更具可维护性和可靠性。
例如,以下是来自真实项目的匿名化依赖关系图
此图中包含 数百个 不同的库。有些是内部库,有些是 OSS 库。这些模块中有一部分每周会发布多个版本。实际上,对于如此规模的图,您无法避免典型的问题,例如
- 多个库提供相同的功能(一个单一的 logger API,但最终得到多个实现)
- 以及更多类似的问题
- 处理运行时不兼容的版本(例如:Scala 2.11 与 Scala 2.12)
- 组件的依赖项错位(例如:Jackson Databind 2.9.0 与 Jackson Core 2.9.4)
- 由于 动态版本升级(版本 “1.+”)导致构建突然失败
- 拒绝有漏洞的传递依赖
- 移除未使用的依赖
- 同一仓库中 子项目 之间的版本不一致
依赖问题会在构建和测试产品时引起许多问题,并且每天都可能极具挑战性地找出导致回归的原因、项目为何突然无法构建,或者哪个依赖项负责另一个依赖项的升级。
如果您幸运的话,您会得到一个编译时错误,但通常只有在执行测试甚至在生产运行时才会看到问题。在所有这些情况下,错误通常很难追溯到源头,因为它是在构建工具中的依赖项解析成功后出现的。因此,从依赖管理角度来看,一切都是正确的,但实际上并非如此。
造成这种不匹配的原因是,解析依赖项的引擎没有足够的信息来检测问题,并在可能的情况下自动修复问题。为了向引擎提供更多信息,模块需要携带更多元数据。好消息是,这是 Gradle 6 的重点!
Gradle 6 依赖管理介绍 #
Gradle 6 向前迈进了一步,是依赖管理新时代的推动者。借助 Gradle 模块元数据,Gradle 现在支持更丰富、更智能的依赖声明模型,这使构建工具能够做出更好的决策,使构建更可靠,并降低维护依赖关系图的成本。
在依赖管理中看到的许多问题通常是消费者(例如,您构建的应用程序)和生产者(例如,您使用的库/依赖项)之间意见不一致的后果,因为依赖管理引擎没有足够的信息来做出好的决策。
至关重要的是,库(例如 Guava)或框架作者(例如 Spring Boot 或内部框架)可以以更丰富的方式表达需求,以便他们的用户面临更少的依赖管理问题。他们应该能够表达诸如“如果您不知道使用哪个版本,请使用此版本”,或者“如果您使用此功能,那么您还需要这些额外的依赖项”之类的需求。这些是 Gradle 6 提供的众多选项中的一部分。
典型的依赖声明以 group
、artifact
和 version
(也称为 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 做什么,包括
- 声明 丰富的版本约束 以更清晰地表达意图,并让引擎找到最佳解决方案
- 使用 平台 执行集中式版本声明。
- 修复不兼容模块版本的问题,也称为 依赖版本对齐。
- 使用 功能集 摆脱臭名昭著的多个 logger 实现。
- 构建和使用具有 可选功能 的库
- 使用 依赖锁定 确保可复现的构建
- 不同类型的 Java 组件:库、应用程序 和 平台
Gradle 6 是朝着更好的依赖管理迈出的重要一步,但开发并没有就此止步:我们知道我们还有很多工作要做,我们将解决您的反馈,请随时提出!