🌱 一、前言:为什么要研究依赖?
写 Java 项目,谁没被 Maven "支配"过呢?
你加了个 Spring Boot Starter,结果一堆库跟着进来;
别人告诉你"scope 写错了";
编译正常但运行报错,或者 jar 包体积暴涨到 200MB。
这一切背后,其实都是 Maven 依赖系统 在发挥作用。
要真正掌握 Maven,就得先搞清楚:
"依赖是什么?"、"它怎么传递?"、"怎么解决冲突?"、"什么时候该 provided?"
🧱 二、依赖的本质:三段坐标
Maven 的核心设计哲学之一是"声明式依赖"。
你不需要手动下载 jar,只要写出三个坐标:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.2</version>
</dependency>
这三个坐标就像一个图书馆的"索书号":
- groupId:组织名(相当于出版社)
- artifactId:模块名(相当于书名)
- version:版本号(相当于第几版)
Maven 仓库 = 全球最大"开源图书馆"
依赖声明 = 你要借的"书目清单"
🧩 三、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 ------ 默认的依赖方式
xml
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
特点:
- 编译、运行、测试全阶段可用;
- 可传递;
- 打包会带上。
适合:核心依赖(比如 Spring Context、Apache Commons)。
2️⃣ provided ------ 编译要用,运行别带
xml
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
适合:由容器(Tomcat、Jetty)或环境提供的类库。
打包带上会冲突。
3️⃣ runtime ------ 运行时才需要的依赖
xml
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.1.0</version>
<scope>runtime</scope>
</dependency>
特点:
- 编译不需要(用接口即可);
- 运行时才加载;
- 打包会带上。
适合:数据库驱动、日志实现等。
4️⃣ test ------ 仅在测试阶段使用
xml
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
不会参与最终打包,测试用完即止。
5️⃣ system ------ 手动指定路径
xml
<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) :
xml
<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 会选择依赖树中路径最短的版本。
例:
less
A → B → commons-lang3:3.12.0
A → C → commons-lang3:3.14.0
A 直接依赖 C 的路径更短,则取 3.14.0。
如果两者路径一样长:
- 则选择 声明顺序靠前 的依赖。
💡 查看依赖树命令:
mvn dependency:tree
可查看传递依赖及冲突来源。
🔧 强制指定版本:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>
</dependencyManagement>
dependencyManagement 只定义版本,不自动引入依赖。
🧩 八、依赖排除(Exclusion)
有时候我们不想要某个传递依赖:
xml
<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 游戏一样,你给每个依赖分配"职业技能",
打包、传递、运行都明明白白,不再踩坑!