第4章:查看依赖树 —— 用 gradle dependencies 看清所有关系
上一章我们聊了传递依赖的规则,知道了冲突是怎么冒出来的。但光知道原理不够,真到了项目里,你还是得肉眼去看——到底哪些依赖被拉进来了?谁把我想要的版本覆盖了?
这时候就得请出 gradle dependencies 这个命令。我个人习惯,每接手一个新项目,第一件事就是跑一遍这个命令。它就像项目的「依赖X光机」,所有依赖关系一览无余。
4.1 基本用法:一行命令,看透依赖树
打开终端,进到项目根目录,执行:
./gradlew dependencies
就会输出一大串树形结构的东西。如果你只想看某个具体配置(比如 compileClasspath 或 runtimeClasspath),可以加 --configuration 参数:
./gradlew dependencies --configuration compileClasspath
我在项目里经常只盯 compileClasspath,因为大部分编译期冲突都发生在这里。运行期的问题,再去看 runtimeClasspath。
4.2 怎么读懂这个树?
输出内容看着挺长,但结构其实很简单。每个依赖一行,缩进表示层级。举个例子:
compileClasspath - Compile classpath for source set 'main'.
+--- com.google.guava:guava:30.1-jre -> 30.0-jre
+--- org.slf4j:slf4j-api:1.7.30
\--- org.springframework.boot:spring-boot-starter-web:2.5.4
+--- org.springframework.boot:spring-boot-starter:2.5.4
| +--- org.springframework.boot:spring-boot:2.5.4
| | +--- org.springframework:spring-core:5.3.10
| | \--- org.springframework:spring-jcl:5.3.10
| \--- ...
\--- org.springframework:spring-webmvc:5.3.10
\--- ...
注意看 guava:30.1-jre -> 30.0-jre 这个符号。箭头表示「实际解析后的版本」——你声明了 30.1,但 Gradle 最终给了你 30.0。为什么?因为有别的依赖强制要求 30.0,或者你配置了某个版本锁定策略。
我曾经在一个大型微服务项目里,发现 jackson-databind 被降级了,就是因为 hibernate-validator 依赖了一个更老的版本。光靠直觉根本想不到,一看树就明白了。
4.3 常用变体:只看你关心的
项目配置很多,你不可能每次都看完整输出。下面这几个参数我几乎天天用:
| 参数 | 作用 | 示例 |
|---|---|---|
--configuration |
只看某个配置的树 | dependencies --configuration runtimeClasspath |
--graph |
只显示解析后的最终版本(简明模式) | dependencies --graph |
--info 或 -i |
显示更详细的日志,包括冲突解决过程 | dependencies -i |
那如果我们只想知道某个具体库的依赖关系呢?比如我想查 spring-boot-starter-web 到底拉了哪些东西:
./gradlew dependencies --configuration compileClasspath | grep spring-boot-starter-web -A 20
嗯,土办法,但管用。更正规的做法是用 dependencyInsight 命令,我们下一章会细讲。
4.4 实战:用依赖树定位冲突
假设你项目里有两个库同时引用了 commons-logging:commons-logging,一个要 1.1.3,一个要 1.2。你跑完 dependencies,看到类似:
+--- com.mycompany:my-lib:1.0
| \--- commons-logging:commons-logging:1.1.3 -> 1.2
+--- org.thirdparty:third-lib:2.0
\--- commons-logging:commons-logging:1.2
箭头 -> 1.2 表示最终选了 1.2。如果你怀疑 1.2 有问题,想强制用 1.1.3 怎么办?那就得用 force,或者 strictly,或者排除依赖——具体怎么做,后面章节会一一拆解。
我遇到过最头疼的情况是「树里根本看不到某个库」,但编译却报找不到类。这时通常是因为那个库被什么规则的 exclude 排除了。解决办法是加上 --scan 参数生成构建扫描,那个页面会把所有排除规则列得清清楚楚。
4.5 避坑指南
- 只在 IDEA 的 External Libraries 选项卡里看依赖。那个视图有时候会隐藏掉冲突信息,它会自动帮你「合并」了,你根本看不到降级。千万别全信它。
- 在 CI 构建里忘记加
--no-daemon。Gradle Daemon 会缓存输出,有时你改动了 build.gradle,但依赖树还是旧的。建议在 CI 脚本里先用--stop杀掉 Daemon,或者直接用--no-daemon。 - 只看
compileOnly配置的树就以为万事大吉。别忘了运行时依赖也会引起冲突,尤其像 Logback、SLF4J 这些。
4.6 依赖树分析流程图
下面这张图总结了我们从执行命令到定位问题的完整思路:
4.7 小结:依赖树是你的第一道防线
说白了,gradle dependencies 就是解决冲突前必看的「体检报告」。你在写排除语句、加 force、或者改版本策略之前,一定要先跑一遍它。
我见过不少新手,直接在 build.gradle 里写 force = true,结果编译过了但运行时类加载失败——因为他们根本没看依赖树,不知道那个被 force 的版本其实跟其他组件不兼容。
dependencies --configuration compileClasspath,确认没有意外的版本升降。花 10 秒看看树,能省掉后面几小时的排查时间。