依赖传递
本章承接
dependencies,深入讲解 Maven 依赖管理中最具魔力的特性——传递依赖。理解传递机制,是排查"为什么项目里出现了我没声明的 JAR"、"为什么打包体积突然膨胀"等问题的关键。
核心机制
当你声明了对项目 A 的依赖,而项目 A 又声明了对项目 B 的依赖,Maven 会自动将项目 B 引入你的项目,无需你在 POM 中显式声明 B。
这句话的潜台词是:依赖关系具有传染性。你只需关心你的"直接朋友"(直接依赖),Maven 帮你处理"朋友的朋友"(传递依赖)。
什么是传递依赖?
假设你的项目 employee-system 依赖了 spring-context,而 spring-context 内部又依赖了 spring-core、spring-beans、spring-aop。在 Maven 中,你只需声明 spring-context,其余三个会自动进入你的 classpath——这就是传递依赖。
传递规则:哪些 scope 会传递?
并非所有依赖范围都会传递。Maven 的传递规则如下:
| 直接依赖 scope | 传递依赖的 scope | 是否传递 |
|---|---|---|
compile | compile | ✅ 传递 |
compile | test | ❌ 不传递 |
test | 任何 | ❌ 不传递 |
provided | 任何 | ❌ 不传递 |
runtime | runtime | ✅ 传递 |
system | 任何 | ❌ 不传递 |
核心原则:
compile和runtime范围的依赖会传递test、provided、system范围的依赖不会传递- 传递后的 scope 可能发生变化(如
compile→runtime的传递结果是runtime)
传递的利与弊
利:
- 减少重复声明:你不需要知道
spring-context内部依赖了什么,只需关心它本身 - 版本一致性:
spring-context依赖的spring-core版本由 Spring 团队锁定,避免你自己拼错版本 - 升级原子性:升级
spring-context时,其整个传递树一起升级,不会出现版本碎片
弊:
- 依赖膨胀:一个
spring-boot-starter-web可能引入 50+ 个传递依赖,打包体积剧增 - 隐式冲突:两个直接依赖分别传递了不同版本的同一个库,导致 classpath 冲突
- 黑盒风险:你不知道项目里某个 JAR 从哪来,排查问题时需要
mvn dependency:tree
生活类比:搬家时的"附带物品"
想象你租了一套带家具的公寓(依赖 spring-context):
- 利:房东已经配好了床、沙发、冰箱(传递依赖
spring-core、spring-beans)。你拎包入住,不需要自己去家具城一件件买。 - 弊:你发现公寓里有一台你不需要的跑步机(多余的传递依赖),占用了客厅空间(打包体积膨胀)。更糟的是,房东配的冰箱和你自己买的冰箱(另一个依赖传递的冲突版本)同时存在,导致电路过载(classpath 冲突)。
Maven 的传递依赖就像这套带家具的公寓——方便,但你需要定期检查"家具清单",确保没有多余或冲突的物品。
图示
传递依赖示意图
上图展示了 spring-context 的传递树:你的项目只声明了 spring-context,但 Maven 自动引入了 spring-core、spring-beans、spring-aop、spring-expression,而 spring-core 又进一步传递了 spring-jcl。这个树状结构完全由 Maven 自动推导,你无需在 POM 中写任何额外配置。
完整示例
场景
飞翔科技的 employee-system 引入了 spring-context 和 mybatis。后端小崔发现项目里出现了一些他没声明的 JAR,怀疑是传递依赖所致。架构师白歌要求他验证传递前后的 classpath 变化,并理解传递规则。
操作前:无传递依赖的 classpath
假设 Maven 没有传递机制,小崔的 pom.xml 只能声明直接依赖:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.21</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.9</version>
</dependency>
</dependencies>
执行编译:
mvn compile
结果(假设无传递机制):编译失败,报错 ClassNotFoundException: org.springframework.core.io.Resource。
原因:spring-context 的代码里 import 了 spring-core 的类,但 classpath 里没有 spring-core。小崔必须手动追踪 spring-context 依赖了哪些库,逐一声明——这正是 Maven 试图消灭的重复劳动。
操作后:传递依赖自动补齐
在真实的 Maven 中,执行同样的 pom.xml 和命令:
mvn dependency:tree
输出:
com.feixiang:employee-system:jar:1.0.0
+- org.springframework:spring-context:jar:5.3.21:compile
| +- org.springframework:spring-aop:jar:5.3.21:compile
| +- org.springframework:spring-beans:jar:5.3.21:compile
| +- org.springframework:spring-core:jar:5.3.21:compile
| | \- org.springframework:spring-jcl:jar:5.3.21:compile
| \- org.springframework:spring-expression:jar:5.3.21:compile
\- org.mybatis:mybatis:jar:3.5.9:compile
+- org.slf4j:slf4j-api:jar:1.7.32:compile
\- org.javassist:javassist:jar:3.27.0-GA:compile
classpath 变化对比:
| 场景 | classpath 中的 JAR 数量 | 小崔需要声明的依赖数 |
|---|---|---|
| 无传递机制(假设) | 2 | 8+(需手动追踪所有间接依赖) |
| Maven 传递依赖(真实) | 8 | 2(只需声明直接依赖) |
变化分析:
- 小崔只声明了 2 个依赖,Maven 自动解析出 8 个 JAR 进入 classpath
spring-core的spring-jcl是第二层传递,同样自动引入mybatis传递了slf4j-api和javassist,小崔完全不需要知道它们的存在- 李眉部署时,这 8 个 JAR 都会被打包进最终产物(因为 scope 都是
compile)
反例:test 依赖不传递
小崔在 pom.xml 中加一个 test 范围的依赖:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.6.1</version>
<scope>test</scope>
</dependency>
执行:
mvn dependency:tree
输出:
com.feixiang:employee-system:jar:1.0.0
+- org.springframework:spring-context:jar:5.3.21:compile
| +- ...
\- org.mockito:mockito-core:jar:4.6.1:test
+- net.bytebuddy:byte-buddy:jar:1.12.10:test
+- net.bytebuddy:byte-buddy-agent:jar:1.12.10:test
\- org.objenesis:objenesis:jar:3.2:test
注意 mockito-core 及其传递依赖(byte-buddy、objenesis)的 scope 都是 test。如果另一个项目 payroll-service 依赖了 employee-system,这些 test 依赖不会传递过去——这是 Maven 的隔离机制,确保测试工具不会污染生产环境。
易错点与常见问题
误区一:"我没声明这个 JAR,所以可以删掉"
错误认知:"mvn dependency:tree 里出现了 javassist,我没声明它,把它从 pom.xml 里删掉。"
纠正:javassist 是 mybatis 的传递依赖,不是你直接声明的。你"删掉"它的唯一方式是排除 mybatis 对 javassist 的依赖(用 exclusions),或者不依赖 mybatis。如果你直接删除 mybatis,javassist 自然消失;但如果你需要 mybatis,就必须接受它的传递树(或精确排除某些分支)。
误区二:传递依赖的版本我可以随意覆盖
错误认知:"spring-context 传递了 spring-core 5.3.21,我在 dependencies 里直接声明 spring-core 5.3.22,就能单独升级它。"
纠正:这种做法技术上可行,但逻辑上危险。spring-context 5.3.21 是在 spring-core 5.3.21 上测试的,强行把 spring-core 升到 5.3.22 可能导致二进制不兼容。正确的做法是整体升级 spring-context 到 5.3.22,让它的传递树一起升级。传递依赖的价值之一就是"版本锁定的一致性"。
误区三:所有传递依赖都会被打包
错误认知:"mvn package 会把 dependency:tree 里看到的所有 JAR 都打进最终产物。"
纠正:只有 compile 和 runtime 范围的依赖会进入打包产物。test、provided 范围的依赖不会。此外,如果你使用 optional 或 exclusions,传递树会被修剪。打包内容取决于有效依赖树,而非 dependency:tree 的原始输出。
小结
传递依赖是 Maven 依赖管理的核心机制。它通过自动解析"朋友的朋友",将开发者从繁琐的间接依赖追踪中解放出来。compile 和 runtime 范围的依赖会传递,test 和 provided 不会。传递带来便利的同时,也可能导致依赖膨胀和隐式冲突,需要配合 mvn dependency:tree 定期审查依赖树。
本章与全局的关系:本章解释了"依赖为什么会自动传播"。下一章"scope"将讲解"依赖在什么时候、什么场景下生效"——这是控制传递边界的关键工具。