一、前言:为什么要研究依赖?
写 Java 项目,谁没被 Maven “支配”过呢?
你加了个 Spring Boot Starter,结果一堆库跟着进来;
别人告诉你“scope 写错了”;
编译正常但运行报错,或者 jar 包体积暴涨到 200MB。
这一切背后,其实都是 Maven 依赖系统 在发挥作用。
要真正掌握 Maven,就得先搞清楚:
二、依赖的本质:三段坐标
Maven 的核心设计哲学之一是“声明式依赖”。
你不需要手动下载 jar,只要写出三个坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.2</version>
</dependency>
这三个坐标就像一个图书馆的“索书号”:
- groupId:组织名(相当于出版社)
- artifactId:模块名(相当于书名)
- version:版本号(相当于第几版)
| 元素 | 含义 |
|---|---|
|
组织或公司标识 |
|
模块名称 |
|
版本号 |
|
依赖作用范围(compile、provided、runtime…) |
|
是否为可选依赖 |
|
排除指定传递依赖 |
三、Maven 的依赖来源
Maven 在解析依赖时,会按照以下顺序查找 jar 包:
- 本地仓库(
~/.m2/repository)
→ 最近一次构建下载过的包会被缓存到这里。 - 远程中央仓库(
https://repo.maven.apache.org/maven2/)
→ Maven 官方中央仓库。 - 私有仓库(公司 Nexus / Artifactory)
→ 企业内部维护的依赖镜像。
Maven 会自动从上往下找,找不到就报错:
四、依赖范围(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 | 传递 |
简单理解:
️ 七、依赖冲突与解决策略
当两个不同版本的相同依赖出现时:
- 最近路径优先(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) | 方便升级维护 |
记忆口诀:
像玩 RPG 游戏一样,你给每个依赖分配“职业技能”,
打包、传递、运行都明明白白,不再踩坑!
十、 —— 控制“依赖传递”的另一种方式
现在我们聊聊另一个常被忽略的兄弟:。
用于告诉 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
| 特征 | true |
provided |
|---|---|---|
| 控制对象 | 依赖传递 | 生命周期 |
| 编译期可见 | ||
| 运行期可见 | (环境提供) | |
| 传递性 | 不传递 | 不传递 |
| 场景 | 模块设计、SDK | Web 环境、容器依赖 |
通俗地说:
scope决定“何时使用”;optional决定“要不要传下去”。
十一、依赖冲突与解决规则
Maven 在面对同一个依赖的多个版本时,遵循两条核心规则:
- 最近路径优先(Nearest Definition Wins)
—— 谁离当前模块更近,用谁。 - 先声明优先(First Declaration Wins)
—— 同层级冲突时,谁先写谁赢。
可通过以下命令查看依赖树:
mvn dependency:tree
十二、全景图:Maven 依赖生命周期与传递机制(附图)
十四、总结与金句彩蛋
| 元素 | 控制内容 | 核心作用 |
|---|---|---|
|
生命周期 | 控制在哪些阶段可见 |
|
传递性 | 决定是否下游继承 |
|
精准排除 | 清理依赖树 |
一句话记忆:
尾声:让依赖管理优雅如诗
每次写 ,都像在雕琢项目的骨架。
当你真正理解 scope、optional 与传递关系的微妙平衡,
你就离“构建大师”更近一步了。
