第3章:实战——查看项目依赖树与锁定文件

3.1 为什么需要查看依赖树与锁定文件?

在治理依赖冲突之前,必须先“看见”依赖。Gradle 提供 dependencies 和 dependencyInsight 两个内置任务,分别用于输出完整依赖树和特定依赖的溯源路径。锁定文件 gradle.lockfile 则记录项目所有依赖的精确版本,是保证构建可重现、防止版本漂移的关键产物。本章以实战方式演示这三者的使用方法、输出解析和实际冲突排查场景。

核心原则:先看树(依赖全貌),再查细节(冲突来源),最后锁定版本(固化依赖)。

3.2 查看项目依赖树:dependencies 任务

dependencies 任务以树状输出当前项目的所有配置依赖。常用命令如下:

# 查看所有配置的依赖树
./gradlew dependencies

# 仅查看 compileClasspath 配置的依赖(推荐)
./gradlew dependencies --configuration compileClasspath

# 使用 Gradle Wrapper
gradlew dependencies --configuration runtimeClasspath

输出示例(部分):

------------------------------------------------------------
Root project
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
\--- com.google.guava:guava:30.1-jre
     +--- com.google.guava:failureaccess:1.0.1
     \--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava

runtimeClasspath - Runtime classpath for source set 'main'.
\--- com.google.guava:guava:30.1-jre -> 31.0.1-jre (由其他依赖提升版本)
     +--- com.google.guava:failureaccess:1.0.1
     \--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava

关键符号:

  • -> 表示版本被提升(越级),通常由冲突解决策略导致。
  • (c) 表示该依赖已被约束(constraint)。
  • (n) 表示该依赖不可传递(not transitive)。
  • (*) 表示该依赖被忽略或重复。
常见错误:如果直接执行 gradlew dependencies 不加 --configuration,输出会非常冗长(包含 test、jacoco 等所有配置)。建议每次指定目标配置,如 compileClasspath 或 runtimeClasspath。

3.3 依赖溯源:dependencyInsight 任务

当在依赖树中发现某个依赖版本异常或出现冲突时,使用 dependencyInsight 可以定位该依赖的声明路径和冲突来源。

# 查看具体依赖的详情(需指定依赖名称和配置)
./gradlew dependencyInsight --dependency guava --configuration compileClasspath

# 支持部分匹配,如 --dependency com.google
gradlew dependencyInsight --dependency guava --configuration runtimeClasspath

输出示例:

com.google.guava:guava:30.1-jre (by constraint)
   variant "compile" (optional=true)
      \--- project :app (requested 30.1-jre)

com.google.guava:guava:31.0.1-jre (by conflict resolution)
   variant "compile"
      \--- project :lib (requested 31.0.1-jre)
      \--- project :app (requested 30.1-jre) -> 31.0.1-jre

从输出可清晰看到:app 直接声明 30.1-jre,lib 声明 31.0.1-jre,Gradle 通过冲突解决选择了 31.0.1-jre。

实用场景:当构建日志出现 Conflict found 警告,或运行时出现 NoSuchMethodError,立即用 dependencyInsight 查看是哪个上游依赖拉入了冲突版本。

调试技巧:在 build.gradle 中临时添加 configurations.all { resolutionStrategy { failOnVersionConflict() } },然后重新运行 dependencies,Gradle 会直接终止并报告冲突位置,配合 dependencyInsight 可快速定位。

3.4 锁定文件 gradle.lockfile:生成与解析

3.4.1 启用依赖锁定

在 build.gradle 的 dependencyLocking 块中配置:

dependencyLocking {
    lockAllConfigurations()
    lockMode = LockMode.STRICT   // 严格模式,安装时检查锁定文件
    ignoredDependencies = []      // 可忽略某些动态版本
}

然后在 settings.gradle 中应用 java 插件后即可生效(Gradle 6.8+ 原生支持)。

3.4.2 生成锁定文件

# 生成或更新 gradle.lockfile
./gradlew generateLockFiles

# 或使用 --write-locks 标志
gradlew dependencies --write-locks

生成的文件位于项目根目录,内容示例:

# This is a Gradle generated file for dependency locking.
com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath
com.google.guava:guava:30.1-jre=compileClasspath
com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath
empty=

格式解析:每行 group:name:version=configuration1,configuration2,表示该依赖在指定配置下的锁定版本。锁定文件必须提交到版本控制,确保所有开发者使用完全相同的依赖版本。

性能优化:依赖锁定文件只锁定模块版本,不锁定传递依赖内部的路径。如果频繁修改依赖,可使用 --refresh-dependencies 结合 generateLockFiles 避免重复解析网络。

3.4.3 锁定文件与依赖树的关系

锁定文件生成后,每次构建(特别是增量构建)都会校验当前解析的依赖是否与锁定文件一致。若不一致(例如新依赖引入后未更新锁定文件),构建会失败并提示。

通过比较 gradle.lockfile 与 dependencies 输出,可以快速发现:

  • 是否有未汇报的版本变化(锁定文件与实际解析版本不一致)。
  • 是否存在重复/冲突依赖(锁定文件中同一模块出现多行)。
  • 锁定配置是否覆盖了所有运行时配置(如 compileClasspath、runtimeClasspath 等)。

3.5 依赖树与锁定文件的协同工作流程

下面的流程图展示了从查看依赖树到锁定版本的完整步骤:

依赖树 & 锁定文件协同工作流程 build.gradle 依赖声明 dependencies 任务 依赖树(含冲突 提升标记) 出现版本冲突 dependencyInsight 定位冲突根因 调整声明/约束 build.gradle 添加 dependencyLocking generateLockFiles 任务 gradle.lockfile 重新运行构建 检查一致性 说明:锁定文件生成后,每次构建会自动校验;如需更新依赖,重复执行 generateLockFiles。

3.6 常见错误及其处理

现象原因解决方案
gradle.lockfile 缺失导致构建失败 启用锁定后未生成文件
./gradlew generateLockFiles
dependencies 输出太长难以阅读 未指定配置 始终添加 --configuration compileClasspath 等
dependencyInsight 找不到依赖 依赖名称拼写或 --configuration 不匹配 先用 dependencies 确认配置和完整 group:name
锁定文件内容与构建实际版本不一致 手动修改锁定文件或依赖变更后未重新生成 ./gradlew generateLockFiles --write-locks

3.7 性能与稳定性优化建议

  • 避免频繁生成锁定文件:只在依赖声明变化后执行 generateLockFiles,日常增量构建不触发。
  • 使用容器化 CI:在 CI 环境中保留 gradle.lockfile 并缓存 ~/.gradle/caches,减少网络解析耗时。
  • 分配置锁定:如果项目有上百个依赖,只锁定 compileClasspath 和 runtimeClasspath,跳过不常用的 test 配置,加快锁定文件生成。
  • 结合 --build-cache:在获取依赖树和锁定文件时启用构建缓存,避免每次全量解析。

生产级实践:将 gradle.lockfile 纳入代码评审,任何 PR 若修改了依赖版本,锁定文件必须同步更新。配合 dependencies 与 dependencyInsight 的 CI 检查步骤,确保合并前依赖无冲突、版本一致。