解决依赖问题

版本冲突可能是一个很难解决的问题。 如果您的项目处理许多依赖项,并且您选择对传递依赖项使用自动解析,则版本冲突几乎是不可避免的。 Gradle 解决这些冲突的默认策略是选择依赖项的最新版本。 依赖关系报告是一个非常宝贵的工具,可用于查找为您请求的依赖关系选择了哪个版本。 在下一节中,我将展示如何解决版本冲突问题并根据您的特定用例调整 Gradle 的依赖项解析策略。

应对版本冲突

Gradle 不会自动通知您您的项目处理版本冲突。 必须不断运行依赖性报告来找出答案并不是解决该问题的实际方法。 相反,您可以更改默认解决策略,以便在遇到版本冲突时使构建失败,如以下代码示例所示:

configurations.cargo.resolutionStrategy {
    failOnVersionConflict()
}

失败可能有助于调试目的,特别是在设置项目和更改依赖项集的早期阶段。 运行项目的任何任务也将指示版本冲突,如以下示例输出所示:

$ gradle -q deployToLocalTomcat
FAILURE: Build failed with an exception.
* Where:
Build file '/Users/benjamin/Dev/books/gradle-in-action/code/chapter4/
➥ cargo-dependencies-fail-on-version-conflict/buildxxx.gradle' line: 10
* What went wrong:
Execution failed for task ':deployToLocalTomcat'.
> Could not resolve all dependencies for configuration ':cargo'.
    > A conflict was found between the following modules:
        - xml-apis:xml-apis:1.3.03
        - xml-apis:xml-apis:1.0.b2

丰富的 API 来访问已解析的依赖关系图

在内存中,Gradle 构建已解析依赖图的模型。 Gradle 的解析结果 API 使您可以对请求和选择的依赖项进行更细粒度的控制。 开始熟悉 API 的一个好地方是接口 ResolutionResult。

强制执行特定版本

您需要管理的项目越多,您就越需要标准化构建环境。 您需要共享常见任务或确保所有项目都使用特定版本的库。 例如,您希望统一使用 Cargo 版本 1.3.0 部署的所有 Web 项目,即使依赖项声明可能请求不同的版本。 有了 Gradle,实施这样的企业策略真的很容易。 它使您能够强制执行特定版本的顶级依赖项以及传递依赖项。

以下代码片段演示了如何重新配置配置货物的默认解析策略以强制依赖于 Ant 任务的 1.3.0 版本:

configurations.cargo.resolutionStrategy {
    force 'org.codehaus.cargo:cargo-ant:1.3.0'
}

现在,当您运行依赖性报告任务时,您将看到请求的 Cargo Ant 版本被全局强制的模块版本否决:

$ gradle -q dependencies
------------------------------------------------------------
Root project
------------------------------------------------------------
cargo - Classpath for Cargo Ant tasks.
\--- org.codehaus.cargo:cargo-ant:1.3.1 -> 1.3.0 // 强制模块版本优先
  \--- org.codehaus.cargo:cargo-core-uberjar:1.3.0
      +--- ...

使用依赖性洞察报告

如前所述,对配置解析策略的更改完美地放置在初始化脚本中,因此可以在全局级别上强制执行。 构建脚本用户可能不知道为什么选择这个特定版本的 Cargo Ant 任务。 他们唯一看到的是依赖性报告表明选择了不同的版本。 有时您可能想知道是什么迫使选择此版本。

Gradle 提供了一种不同类型的报告:依赖关系洞察报告,它解释了依赖关系如何以及为何出现在图表中。 要运行报告,您需要提供两个参数:配置名称(默认为编译配置)和依赖项本身。 以下对帮助任务 dependencyInsight 的调用显示了原因,以及依赖项 xml-apis:xml-apis 的请求和选定版本:

$ gradle -q dependencyInsight --configuration cargo --dependency xml-apis:xml-apis
xml-apis:xml-apis:1.3.03 (conflict resolution)
+--- org.codehaus.cargo:cargo-core-uberjar:1.3.0
| \--- org.codehaus.cargo:cargo-ant:1.3.0
| \--- cargo
\--- xerces:xercesImpl:2.8.1
\--- org.codehaus.cargo:cargo-core-uberjar:1.3.0 (*)
xml-apis:xml-apis:1.0.b2 -> 1.3.03
\--- dom4j:dom4j:1.4
\--- org.codehaus.cargo:cargo-core-uberjar:1.3.0
\--- org.codehaus.cargo:cargo-ant:1.3.0
\--- cargo
(*) - dependencies omitted (listed previously)

依赖关系报告从配置的顶级依赖关系开始,而洞察报告则显示从特定依赖关系开始一直到配置的依赖关系图。 因此,洞察报告代表了常规依赖性报告的倒置视图,如图 5.10 所示。

刷新缓存

为了避免针对特定类型的依赖项反复访问存储库,Gradle 应用了某些缓存策略。 依赖项的快照版本和使用动态版本模式声明的依赖项就是这种情况。 一旦解决,它们就会被缓存 24 小时,从而实现更快、更高效的构建。 工件缓存时间范围到期后,将再次检查存储库,如果工件发生更改,则下载新版本的工件。

image 2024 03 18 12 55 15 425
Figure 1. 图 5.10 不同报告类型的依赖关系图视图

您可以使用命令行选项 --refresh-dependencies 手动刷新缓存中的依赖项。 此标志强制使用配置的存储库检查已更改的工件版本。 如果校验和发生更改,将再次下载依赖项并替换缓存中的现有副本。 一段时间后,必须添加命令行选项可能会变得很累,或者您可能会忘记添加它。 或者,您可以配置构建来更改缓存的默认行为。

假设您一直想要获取使用 org.codehaus.cargo:cargo-ant:1.+ 声明的 Cargo Ant 任务的最新 1.x 版本。 您可以将动态依赖版本的缓存超时设置为 0 秒,如以下代码片段所示:

configurations.cargo.resolutionStrategy {
    cacheDynamicVersionsFor 0, 'seconds'
}

您可能有充分的理由不想缓存外部模块的快照版本。 例如,您组织中的另一个团队正在开发一个在多个项目之间共享的可重用库。 在开发过程中,代码会发生很大变化,您总是希望获得最新且(希望是)最好的代码添加。 以下代码块修改配置的解析策略以根本不缓存快照版本:

configurations.compile.resolutionStrategy {
    cacheChangingModulesFor 0, 'seconds'
}

总结

大多数项目,无论是开源项目还是企业产品,都不是完全独立的。 它们依赖于其他项目构建的外部库或组件。 虽然您可以自己管理这些依赖项,但手动方法无法满足现代软件开发的要求。 项目变得越复杂,就越难弄清楚依赖项之间的关系,解决潜在的版本冲突,甚至知道为什么需要特定的依赖项。

通过自动依赖项管理,您可以通过项目中的唯一标识符声明依赖项,而无需手动接触工件。 在运行时,依赖的工件会自动在存储库中解析、下载、存储在本地缓存中,并可供您的项目使用。 自动化依赖管理并非没有挑战。 我们讨论了潜在的陷阱以及如何应对它们。

Gradle 提供了强大的开箱即用的依赖管理。 您学习了如何声明不同类型的依赖项、如何借助配置对它们进行分组,以及如何针对各种类型的存储库来下载它们。 本地缓存是 Gradle 依赖管理基础设施的组成部分,负责高性能和可靠的构建。 我们分析了它的结构并讨论了它的基本特征。 了解如何解决依赖版本冲突并微调缓存是稳定可靠构建的关键。 您使用 Gradle 的依赖关系报告来很好地了解已解析的依赖关系图,以及为什么选择特定版本的依赖关系以及它来自何处。 我展示了更改默认解析策略和缓存行为的策略,以及需要它们的适当情况。

在下一章中,您将通过模块化代码将待办事项应用程序提升到一个新的水平。 您将学习如何使用 Gradle 的多项目构建支持来定义各个组件之间的依赖关系并使它们作为一个整体运行。