传递性依赖机制
传递性依赖是 Maven 最强大的特性之一。传统项目需要手动下载每个 jar 及其依赖的 jar,一个库可能依赖十几个其他库,遗漏任何一个都会导致编译或运行失败。Maven 的传递性依赖机制自动处理这些依赖链,只需声明直接依赖,间接依赖自动引入。
为什么需要传递性依赖
传统方式的痛点
Bash
场景:项目需要使用 Spring框架
手动管理依赖:
1. 下载 spring-context-5.3.20.jar
2. 启动报错:ClassNotFoundException: SpringCore
3. 查文档发现需要 spring-core
4. 下载 spring-core-5.3.20.jar
5. 启动报错:ClassNotFoundException: SpringJcl
6. 再下载 spring-jcl-5.3.20.jar
7. 再下载 spring-beans、spring-aop、spring-expression...
问题:
- 不知道一个库依赖哪些其他库
- 需要反复尝试、查文档
- 遗漏依赖导致运行时错误
- 版本不匹配导致兼容问题
- 团队成员可能配置不同
Maven 解决方案
Bash
你只需声明:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.20</version>
</dependency>
Maven 自动完成:
1. 下载 spring-context-5.3.20.jar
2. 解析 spring-context 的 pom.xml
3. 发现 spring-context 依赖:
- spring-core:5.3.20
- spring-beans:5.3.20
- spring-aop:5.3.20
- spring-expression:5.3.20
- spring-jcl:5.3.20
4. 自动下载所有传递依赖
5. 自动管理版本一致性
传递性依赖工作原理
Maven 如何解析传递依赖
Bash
传递依赖解析流程:
步骤1:解析项目 pom.xml
获取直接依赖列表
步骤2:下载直接依赖的 jar 和 pom
每个 jar 都有自己的 pom.xml
步骤3:解析每个依赖的 pom.xml
获取该依赖的依赖列表(即传递依赖)
步骤4:下载传递依赖
同样获取其 pom.xml,继续解析
步骤5:递归解析
直到所有依赖链解析完成
步骤6:构建依赖树
记录每个依赖的来源路径
步骤7:应用调解规则
解决版本冲突(后面详细讲)
依赖的 pom.xml 如何描述其依赖
XML
spring-context-5.3.20.jar 包含的 pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.20</version>
<dependencies>
<!-- Spring Context 声明它依赖的其他 Spring 模块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.20</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.20</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.20</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.20</version>
</dependency>
</dependencies>
</project>
Maven 解析这个 pom.xml,自动获取 spring-context 的依赖
依赖树结构
XML
你的项目 pom.xml:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.20</version>
</dependency>
生成的依赖树:
my-app
└── spring-context:5.3.20← 你声明
├── spring-aop:5.3.20← 自动传递
├── spring-beans:5.3.20← 自动传递
│ └── spring-core:5.3.20 ← 二级传递
├── spring-core:5.3.20 ← 自动传递
│ └── spring-jcl:5.3.20 ← 二级传递
└── spring-expression:5.3.20 ← 自动传递
你获得的所有依赖:
- spring-context:5.3.20(直接)
- spring-aop:5.3.20(一级传递)
- spring-beans:5.3.20(一级传递)
- spring-core:5.3.20(一级传递)
- spring-expression:5.3.20(一级传递)
- spring-jcl:5.3.20(二级传递)
查看传递依赖
使用 dependency:tree 命令
XML
# 查看完整依赖树
mvn dependency:tree
输出示例:
XML
[INFO] com.example:my-app:jar:1.0.0
[INFO] +- org.springframework:spring-context:jar:5.3.20:compile
[INFO] | +- org.springframework:spring-aop:jar:5.3.20:compile
[INFO] | +- org.springframework:spring-beans:jar:5.3.20:compile
[INFO] | | \- org.springframework:spring-core:jar:5.3.20:compile
[INFO] | +- org.springframework:spring-core:jar:5.3.20:compile
[INFO] | | \- org.springframework:spring-jcl:jar:5.3.20:compile
[INFO] | \- org.springframework:spring-expression:jar:5.3.20:compile
[INFO] \- junit:junit:jar:4.13.2:test
[INFO] \- org.hamcrest:hamcrest-core:jar:1.3:test
输出符号含义
| 符号 | 含义 | 说明 |
|---|---|---|
+- | 直接依赖 | 你的 pom.xml 声明的依赖 |
\- | 最后依赖 | 某路径上最后一个依赖 |
| ` | ` | 层级分隔 |
过滤查看特定依赖
XML
# 只看 Spring 相关依赖
mvn dependency:tree -Dincludes=org.springframework:*
# 只看某个依赖的来源
mvn dependency:tree -Dincludes=*:spring-core
# 只看 compile scope 依赖
mvn dependency:tree -Dscope=compile
# 只看 test scope 依赖
mvn dependency:tree -Dscope=test
verbose模式显示冲突
Bash
# 显示被排除的依赖(冲突详情)
mvn dependency:tree -Dverbose
输出:
[INFO] +- spring-context:5.3.20
[INFO] | +- spring-core:5.3.20
[INFO] +- some-lib:1.0.0
[INFO] | +- spring-core:4.3.0 (omitted for conflict: 5.3.20)
↑ 显示被排除版本和原因
传递范围规则—— 重要
scope 对传递的影响
关键理解:直接依赖的 scope 影响传递依赖的 scope。
| 直接依赖 scope | 传递依赖 scope | 说明 |
|---|---|---|
| compile | compile | 正常传递,保持原 scope |
| provided | 不传递 | 只在本项目有效 |
| runtime | runtime | 传递但保持 runtime |
| test | 不传递 | 只在本项目有效 |
传递范围变化详解
XML
规则:传递依赖的 scope = min(直接依赖 scope, 传递依赖原 scope)
示例1:compile → compile → compile
A (compile) → B (compile)
结果:A 获得 B (compile)
示例2:compile → runtime → runtime
A (compile) → B (runtime)
结果:A 获得 B (runtime)
示例3:runtime → compile → runtime
A (runtime) → B (compile)
结果:A 获得 B (runtime) ← 受 A 的 runtime限制
示例4:provided → compile → 不传递
A (provided) → B (compile)
结果:A 不获得 B ← provided 不传递
示例5:test → compile → 不传递
A (test) → B (compile)
结果:A 不获得 B ← test 不传递
provided 和 test 不传递的原因
XML
为什么 provided 不传递?
provided 含义:运行时由容器/外部提供
- Servlet API:Tomcat 提供
- 在你的项目中有效(编译时可用)
- 但依赖你的项目的其他项目:
- 可能用不同的容器
- 容器版本可能不同
- 不应继承你的 provided 配置
示例:
你的项目 (Web项目)
└── servlet-api:4.0.1 (provided) ← Tomcat 9 提供
依赖你的项目的其他项目(可能是非Web项目)
└── 不应继承 servlet-api ← 因为它没有 Tomcat
为什么 test 不传递?
test 含义:仅测试使用
- 只在你的 src/test/java 中使用
- 其他项目不需要你的测试依赖
- 测试依赖通常是私有的
示例:
你的项目
└── junit:4.13.2 (test)
依赖你的项目的其他项目
└── 不应继承 junit ← 它有自己的测试依赖
传递依赖的实际场景
场景1:使用 Spring Boot Starter
XML
<!-- 你只声明一个 starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.0</version>
</dependency>
传递依赖树:
XML
spring-boot-starter-web
├── spring-boot-starter
│ ├── spring-boot
│ ├── spring-boot-autoconfigure
│ ├── spring-boot-starter-logging
│ │ ├── logback-classic
│ │ ├── log4j-to-slf4j
│ │ └── jul-to-slf4j
│ └── snakeyaml
├── spring-boot-starter-tomcat
│ ├── tomcat-embed-core
│ ├── tomcat-embed-el
│ └── tomcat-embed-websocket
├── spring-web
├── spring-webmvc
你获得的所有依赖(30+个):
只需声明一个,自动获得整个 Web 开发栈
场景2:使用 MyBatis
Bash
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
传递依赖:
XML
mybatis:3.5.10
└── 无传递依赖(MyBatis 设计简洁)
如果使用 MyBatis-Spring:
mybatis-spring:2.0.7
├── mybatis:3.5.10
└── spring-context:5.x
└── spring的所有依赖...
场景3:使用 Apache Commons
XML
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
传递依赖:
text
commons-lang3:3.12.0
└── 无传递依赖
commons-lang 设计原则:零依赖,轻量
场景4:使用 Hibernate
text
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.10.Final</version>
</dependency>
传递依赖:
text
hibernate-core:5.6.10.Final
├── jakarta.persistence-api
├── jakarta.transaction-api
├── jboss-logging
├── classmate
├── hibernate-commons-annotations
└── byte-buddy
└── byte-buddy-agent
复杂依赖链:Hibernate依赖多个库
传递依赖的版本冲突
冲突产生原因
text
多条路径引入同一依赖的不同版本:
路径1:项目 → A → commons-lang:2.6
路径2:项目 → B → commons-lang:3.12.0
问题:两个版本的 commons-lang,Maven 只能选一个
Maven调解规则
规则1:最短路径优先
text
项目 → A → B → commons-lang:2.6(路径长度3)
项目 → C → commons-lang:3.12.0(路径长度2)
结果:选择 commons-lang:3.12.0
原因:C 路径更短,优先级更高
规则2:声明顺序优先
text
路径长度相同时:
项目 → A → commons-lang:2.6(路径长度2)
项目 → B → commons-lang:3.12.0(路径长度2)
pom.xml声明顺序决定:
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>A</artifactId> <!-- 先声明 -->
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>B</artifactId> <!-- 后声明 -->
</dependency>
</dependencies>
结果:选择 commons-lang:2.6(A 先声明)
规则3:直接声明优先级最高
text
<!-- 在 pom.xml 直接声明版本 -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>3.12.0</version> <!-- 强制版本 -->
</dependency>
<!-- 其他传递版本被覆盖 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>A</artifactId> <!-- 传递 commons-lang:2.6,被覆盖 -->
</dependency>
冲突排查方法
text
# 查看冲突详情
mvn dependency:tree -Dverbose | grep "omitted for conflict"
# 查看某个依赖的所有版本来源
mvn dependency:tree -Dverbose -Dincludes=groupId:artifactId
控制传递依赖
方式1:排除传递依赖(exclusions)
text
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.20</version>
<exclusions>
<!-- 排除不需要的传递依赖 -->
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-jcl</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 使用自己喜欢的日志库 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
适用场景:
- 不需要某个传递依赖
- 要用其他版本或替代库
- 避免冲突
方式2:可选依赖(optional)
text
<!-- 在依赖库的 pom.xml 中定义 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>optional-feature</artifactId>
<version>1.0.0</version>
<optional>true</optional> <!-- 不传递 -->
</dependency>
效果:
- 本项目可以使用 optional-feature
- 依赖本项目的不获得 optional-feature
适用场景:
- 可选功能依赖
- 用户可选择是否启用
方式3:直接声明覆盖
text
<!-- 直接声明需要的版本 -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>3.12.0</version> <!-- 强制版本,覆盖传递版本 -->
</dependency>
方式4:dependencyManagement统一版本
text
<dependencyManagement>
<dependencies>
<!-- 统一管理版本 -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 传递依赖版本会被覆盖 -->
传递依赖的优势与问题
优势
| 优势 | 说明 |
|---|---|
| 简化依赖声明 | 只需声明直接依赖,间接依赖自动引入 |
| 自动版本管理 | 依赖的依赖使用正确版本 |
| 减少遗漏错误 | 自动引入所有必需依赖 |
| 团队协作一致 | 所有成员获得相同依赖 |
| 版本升级方便 | 升级直接依赖,传递依赖自动升级 |
可能的问题
| 问题 | 说明 | 解决方式 |
|---|---|---|
| 版本冲突 | 多路径引入不同版本 | 直接声明版本、exclusions |
| 不需要的依赖 | 传递了不需要的库 | exclusions 排除 |
| 依赖臃肿 | 依赖树过于庞大 | 检查依赖树,清理多余 |
| 版本过旧 | 传递依赖版本太旧 | 直接声明新版本 |
最佳实践
实践1:定期检查依赖树
text
# 定期执行,保存依赖树
mvn dependency:tree > dependency-tree.txt
# 检查是否有意外的依赖
grep "unexpected" dependency-tree.txt
# 检查依赖数量
wc -l dependency-tree.txt
实践2:显式声明核心传递依赖
text
<!-- dependency:analyze 发现 Used undeclared -->
<!-- 代码直接使用传递依赖 -->
<!-- 建议:显式声明 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
原因:
- 代码直接使用的依赖应该显式声明
- 明确控制版本
- 防止上游依赖变化影响
实践3:使用 BOM统一管理复杂依赖
text
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.7.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
实践4:避免依赖地狱
text
症状:
- dependency:tree 输出几百行
- war 包超过 50MB
- 构建时间过长
解决:
1. 检查是否有不需要的依赖
2. 使用 exclusions 排除多余传递依赖
3. 考虑拆分项目,减少依赖
4. 使用轻量级替代库
常见问题
问题1:ClassNotFoundException 找不到类
text
报错:ClassNotFoundException: SomeClass
诊断:
mvn dependency:tree -Dverbose -Dincludes=*:lib-name
发现:
- lib-name 某版本被选中,但该版本没有 SomeClass
- 另一个版本有 SomeClass,但被排除
解决:
直接声明需要的版本
问题2:依赖版本不是期望的
text
期望:commons-lang:3.12.0
实际:commons-lang:2.6
诊断:
mvn dependency:tree -Dverbose -Dincludes=*:commons-lang
发现:
[INFO] +- A → commons-lang:2.6
[INFO] +- B → commons-lang:3.12.0 (omitted for conflict: 2.6)
解决:
直接声明 commons-lang:3.12.0
问题3:依赖太多,war包太大
text
诊断:
mvn dependency:tree > deps.txt
wc -l deps.txt # 查看依赖数量
检查:
- 是否有不必要的依赖
- 是否有功能重复的库
- 是否可以拆分项目
解决:
exclusions 排除不需要的传递依赖
要点总结
- 传递依赖自动引入:只需声明直接依赖,间接依赖自动管理
- 解析依赖的 pom.xml:每个 jar 的 pom.xml 描述其依赖
- dependency:tree 查看:显示完整依赖树和层级关系
- scope 影响传递:compile/runtime 可传递,provided/test 不传递
- 调解规则:最短路径优先、声明顺序优先、直接声明最高
- exclusions 排除:精确排除不需要的传递依赖
- optional 不传递:可选依赖不会传递给下游
- 直接声明控制版本:显式声明版本覆盖所有传递版本
- 定期检查依赖树:了解依赖来源,发现问题
📝 发现内容有误?点击此处直接编辑