依赖地狱是许多团队的一个大问题。项目和它的依赖图越大,维护它就越难。现有的依赖管理工具提供的解决方案不足以有效地处理这个问题。
Gradle 6旨在提供可操作的工具,帮助处理这类问题,使依赖性管理更可维护和可靠。
以一个真实世界项目的匿名依赖关系图为例:
该图中有数百个不同的库,有些是内部库,有些是OSS库。其中一部分模块每周要发布几个版本。在实践中,对于这种规模的图,你没有办法避免典型的问题,比如:
- 多个库提供相同的功能(一个单一的记录器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 Module Metadata的帮助下,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,因为你知道它有一个直接影响到你的错误。
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 Module Metadata在发布的模块中支持它们,对库的作者和消费者都是可用的。它使需求的表达更加清晰,并使引擎能够计算出最佳的解决方案。
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是向更好的依赖管理迈出的重要一步,但发展不会就此停止:我们知道我们还有很多工作要做,我们会处理你的反馈。