使用 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
中存在 bug) - 库属于一个更大的模块集,这些模块需要共享相同的版本(例如 Jackson Core、Databind、Annotations 等)
- …
例如,Gradle 6 提供了更丰富的模型来表达这些内容
- 你需要严格地将此依赖项限制在
[1.0, 2.0[
范围内(因为它遵循语义版本控制) - 并且在这个范围内,你更喜欢
1.5
(因为你已经测试过这个版本) - 并且你拒绝
1.6
,因为你知道它存在一个直接影响你的 bug
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
,我们现在可以构建失败,因为两个模块不一致。
此外,还有一些信息是生产者无法了解的,因为这些信息是在库发布后才改变的:发现的 bug、漏洞、不正确的传递依赖项等等。这些信息可以随时作为额外的输入推送到依赖项管理引擎!
值得注意的是,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 是朝着更好的依赖管理迈出的重要一步,但开发并未止步于此:我们知道我们还有很多工作要做,我们会解决您的反馈,请不要犹豫!