🌱 一、前言:为什么要研究依赖?
写 Java 项目,谁没被 Maven “支配”过呢?
你加了个 Spring Boot Starter,结果一堆库跟着进来;
别人告诉你“scope 写错了”;
编译正常但运行报错,或者 jar 包体积暴涨到 200MB。
这一切背后,其实都是 Maven 依赖系统 在发挥作用。
要真正掌握 Maven,就得先搞清楚:
“依赖是什么?”、“它怎么传递?”、“怎么解决冲突?”、“什么时候该 provided?”
🧱 二、依赖的本质:三段坐标
Maven 的核心设计哲学之一是“声明式依赖”。
你不需要手动下载 jar,只要写出三个坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.2</version>
</dependency>
这三个坐标就像一个图书馆的“索书号”:
- groupId:组织名(相当于出版社)
- artifactId:模块名(相当于书名)
- version:版本号(相当于第几版)
Maven 仓库 = 全球最大“开源图书馆”
依赖声明 = 你要借的“书目清单”
| 元素 | 含义 |
|---|---|
<groupId> | 组织或公司标识 |
<artifactId> | 模块名称 |
<version> | 版本号 |
<scope> | 依赖作用范围(compile、provided、runtime...) |
<optional> | 是否为可选依赖 |
<exclusions> | 排除指定传递依赖 |
🧩 三、Maven 的依赖来源
Maven 在解析依赖时,会按照以下顺序查找 jar 包:
- 本地仓库(
~/.m2/repository)
→ 最近一次构建下载过的包会被缓存到这里。 - 远程中央仓库(
https://repo.maven.apache.org/maven2/)
→ Maven 官方中央仓库。 - 私有仓库(公司 Nexus / Artifactory)
→ 企业内部维护的依赖镜像。
Maven 会自动从上往下找,找不到就报错:
“Could not resolve dependencies…”
🧠 四、依赖范围(Scope)详解
Scope 是 Maven 的依赖生命周期规则,定义了依赖在哪些阶段可用、是否参与打包、是否传递。
| Scope | 编译时可见 | 测试时可见 | 运行时可见 | 打包带上 | 可传递 | 典型场景 |
|---|---|---|---|---|---|---|
compile | ✅ | ✅ | ✅ | ✅ | ✅ | 默认值,大多数库 |
provided | ✅ | ✅ | ❌ | ❌ | ❌ | 容器已提供(Servlet、Lombok) |
runtime | ❌ | ✅ | ✅ | ✅ | ✅ | JDBC Driver、Logback |
test | ❌ | ✅ | ❌ | ❌ | ❌ | JUnit、Mockito |
system | ✅ | ✅ | ❌ | ❌ | ❌ | 手动指定 jar |
import | — | — | — | — | — | 仅用于依赖管理 |
🧩 五、每种 Scope 的典型示例
1️⃣ compile —— 默认的依赖方式
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
特点:
- 编译、运行、测试全阶段可用;
- 可传递;
- 打包会带上。
适合:核心依赖(比如 Spring Context、Apache Commons)。
2️⃣ provided —— 编译要用,运行别带
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
适合:由容器(Tomcat、Jetty)或环境提供的类库。
打包带上会冲突。
3️⃣ runtime —— 运行时才需要的依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.1.0</version>
<scope>runtime</scope>
</dependency>
特点:
- 编译不需要(用接口即可);
- 运行时才加载;
- 打包会带上。
适合:数据库驱动、日志实现等。
4️⃣ test —— 仅在测试阶段使用
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
不会参与最终打包,测试用完即止。
5️⃣ system —— 手动指定路径
<dependency>
<groupId>com.company</groupId>
<artifactId>internal-lib</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/internal-lib.jar</systemPath>
</dependency>
⚠️ 注意:
- 不推荐使用;
- 不可传递;
- 会破坏构建的可移植性。
6️⃣ import —— 依赖版本管理用
用于在 dependencyManagement 中引入 BOM(Bill of Materials) :
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
它不会引入依赖本身,只是导入一组“版本约定”。
🧩 六、依赖传递机制:Maven 的“层层借书”
假设:
- A → 依赖 B
- B → 依赖 C
则 A 间接依赖了 C(称为传递依赖)。
Maven 的传递规则如下:
| A 的 Scope | B 的 Scope | C 是否传递 | 说明 |
|---|---|---|---|
| compile | compile | ✅ | 默认传递 |
| compile | provided | ❌ | 不传递 |
| provided | compile | ❌ | 不传递 |
| test | 任意 | ❌ | 不传递 |
| runtime | compile/runtime | ✅ | 传递 |
简单理解:
只有“compile”或“runtime”的依赖才会往下传递。
test / provided 不会传递。
⚔️ 七、依赖冲突与解决策略
当两个不同版本的相同依赖出现时:
- 最近路径优先(Nearest Definition Wins)
→ Maven 会选择依赖树中路径最短的版本。
例:
A → B → commons-lang3:3.12.0
A → C → commons-lang3:3.14.0
A 直接依赖 C 的路径更短,则取 3.14.0。
如果两者路径一样长:
- 则选择 声明顺序靠前 的依赖。
💡 查看依赖树命令:
mvn dependency:tree
可查看传递依赖及冲突来源。
🔧 强制指定版本:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>
</dependencyManagement>
dependencyManagement 只定义版本,不自动引入依赖。
🧩 八、依赖排除(Exclusion)
有时候我们不想要某个传递依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
比如:自己要用 Undertow 或 Jetty,而不想要 Tomcat。
🧩 九、最佳实践总结
| 场景 | Scope 建议 | 原因 |
|---|---|---|
| 普通库依赖 | compile | 默认 |
| 容器内置库(Servlet、JSP) | provided | 环境已提供 |
| 运行时驱动(JDBC、日志实现) | runtime | 只运行时用 |
| 测试框架 | test | 不参与打包 |
| 编译工具(Lombok、MapStruct) | provided | 编译期生效 |
| 公司内部 jar | system(慎用) | 构建可移植性差 |
| 统一管理版本 | import(BOM) | 方便升级维护 |
依赖管理,是项目整洁性的基石。
依赖范围(Scope)决定了依赖的“生死周期”;
依赖传递规则决定了“家族关系”;
依赖冲突解决机制,则是 Maven 的“江湖规矩”。
💡 记忆口诀:
compile → 全能型
provided → 借用不打包
runtime → 运行才用
test → 只在测试用
像玩 RPG 游戏一样,你给每个依赖分配“职业技能”,
打包、传递、运行都明明白白,不再踩坑!
🧩 十、<optional> —— 控制“依赖传递”的另一种方式
现在我们聊聊另一个常被忽略的兄弟:<optional>。
<optional> 用于告诉 Maven:
“我这里用了一个依赖,但我不想把它传递给下游项目。”
例子:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
<optional>true</optional>
</dependency>
这意味着:
- 当前模块能用
slf4j-simple; - 但依赖此模块的下游项目不会自动拿到它;
- 如果想用,必须手动声明。
✅ 使用场景
| 场景 | 是否适合 |
|---|---|
| SDK、框架模块 | ✅ 非常推荐 |
| Spring Boot Starter | ✅ 常用 |
| 应用层 | ⚠️ 一般不用 |
| 工具类库 | ❌ 不推荐 |
⚔️ optional vs provided
| 特征 | <optional>true</optional> | <scope>provided</scope> |
|---|---|---|
| 控制对象 | 依赖传递 | 生命周期 |
| 编译期可见 | ✅ | ✅ |
| 运行期可见 | ✅ | ❌(环境提供) |
| 传递性 | ❌ 不传递 | ❌ 不传递 |
| 场景 | 模块设计、SDK | Web 环境、容器依赖 |
📎 通俗地说:
scope决定“何时使用”;optional决定“要不要传下去”。
🧩 十一、依赖冲突与解决规则
Maven 在面对同一个依赖的多个版本时,遵循两条核心规则:
- 最近路径优先(Nearest Definition Wins)
—— 谁离当前模块更近,用谁。 - 先声明优先(First Declaration Wins)
—— 同层级冲突时,谁先写谁赢。
可通过以下命令查看依赖树:
mvn dependency:tree
🧠 十二、全景图:Maven 依赖生命周期与传递机制(附图)
📊 下图展示了 Scope 对编译、运行、测试、打包阶段的影响,以及依赖传递与冲突决策逻辑。
🧾 十四、总结与金句彩蛋 🎁
| 元素 | 控制内容 | 核心作用 |
|---|---|---|
<scope> | 生命周期 | 控制在哪些阶段可见 |
<optional> | 传递性 | 决定是否下游继承 |
<exclusions> | 精准排除 | 清理依赖树 |
🎯 一句话记忆:
<scope>管“时间”,
<optional>管“范围”,
<exclusions>管“洁癖”。
💬 尾声:让依赖管理优雅如诗 🌸
“优秀的 Maven 工程,不是依赖越多越强,
而是边界清晰、传递干净、结构优雅。”
每次写 <dependency>,都像在雕琢项目的骨架。
当你真正理解 scope、optional 与传递关系的微妙平衡,
你就离“构建大师”更近一步了。