使用 Gradle 6 避免依赖地狱
引言
依赖地狱是许多团队面临的一个大问题。项目及其依赖图越大,维护起来就越困难。现有依赖管理工具提供的解决方案不足以有效解决此问题。
Gradle 6 旨在提供可操作的工具,帮助解决此类问题,使依赖管理更易于维护和可靠。
例如,这是一个来自真实项目的匿名依赖图
此图中包含**数百**个不同的库。有些是内部库,有些是开源库。其中一部分模块每周都会发布多个版本。在实际操作中,面对如此规模的图,您无法避免以下典型问题:
- 多个库提供相同的功能(单个日志 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 做什么,包括:
- 声明丰富的版本约束,更清晰地表达意图,让引擎找到最佳解决方案
- 使用平台执行集中式版本声明。
- 解决不兼容模块版本问题,也称为依赖版本对齐。
- 使用功能摆脱臭名昭著的多个日志实现。
- 构建和使用具有可选功能的库
- 使用依赖锁定确保使用动态版本构建的可重现性
- Java 组件的不同类型:库、应用程序和平台
Gradle 6 是迈向更好依赖管理的重要一步,但开发并没有止步于此:我们知道我们仍有大量工作要做,我们将解决您的反馈,请不要犹豫!