依赖范围详解
依赖范围(scope)是 Maven 依赖管理中最容易被忽视但最重要的配置。错误设置 scope 会导致打包臃肿、运行时缺少依赖、版本冲突等问题。理解 scope 机制是正确管理依赖的关键。
为什么需要依赖范围
问题场景
XML
场景:Web 项目需要 Servlet API
错误配置1:使用 compile(默认)
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<!-- scope 默认 compile -->
</dependency>
结果:
- servlet-api.jar 打包进 war
- Tomcat 也有自己的 servlet-api
- 两份 servlet-api 可能版本不同
- 运行时类加载冲突,报错!
错误配置2:使用 test
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>test</scope>
</dependency>
结果:
- 编译时找不到 javax.servlet.*
- 代码编译失败!
Maven 解决方案
scope 控制依赖在不同阶段的可见性:
XML
Maven 有三套 classpath:
1. 编译 classpath:编译源码时使用
2. 测试 classpath:编译和运行测试时使用
3. 运行 classpath:实际运行应用时使用
scope 决定依赖出现在哪些 classpath
四种依赖范围详解
scope 对 classpath 的影响
| scope | 编译 classpath | 测试 classpath | 运行 classpath | 是否打包 |
|---|---|---|---|---|
| compile(默认) | ✓ 包含 | ✓ 包含 | ✓ 包含 | ✓ 打包 |
| provided | ✓ 包含 | ✓ 包含 | ✗ 不包含 | ✗ 不打包 |
| runtime | ✗ 不包含 | ✓ 包含 | ✓ 包含 | ✓ 打包 |
| test | ✗ 不包含 | ✓ 包含 | ✗ 不包含 | ✗ 不打包 |
compile 范围(默认)
特点:全程参与,最常用的范围。
XML
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.20</version>
<scope>compile</scope> <!-- 默认值,可省略 -->
</dependency>
实际行为:
XML
编译时:spring-core.jar 在 classpath,编译通过
测试时:spring-core.jar 在 classpath,测试可用
运行时:spring-core.jar 在 classpath,运行可用
打包时:spring-core.jar 打包进最终产物
传递时:传递给依赖此项目的其他项目
适用场景:
| 场景 | 说明 |
|---|---|
| Spring 框架 | 项目核心依赖,全程需要 |
| Jackson JSON | 业务代码直接使用 |
| MyBatis | ORM 框架,运行时必需 |
| HikariCP | 连接池,运行时必需 |
provided 范围—— 重要
特点:编译可用,运行时由外部提供,不打包。
XML
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope> <!-- 关键:不打包 -->
</dependency>
实际行为:
XML
编译时:servlet-api.jar 在 classpath,编译通过
测试时:servlet-api.jar 在 classpath,测试可用
运行时:servlet-api.jar 不在 classpath(Tomcat 提供)
打包时:servlet-api.jar 不打包进 war
传递时:不传递给依赖此项目的其他项目
为什么这样设计:
text
Servlet API 由 Tomcat 容器提供:
项目编译:需要 servlet-api.jar 才能编译 Servlet 代码
项目运行:Tomcat 已加载自己的 servlet-api.jar
如果项目也打包一份:
- Tomcat 的 servlet-api(如 4.0.1)
- war 里的 servlet-api(如 3.1.0)
- 两份 jar,类加载器冲突
- 报错:ClassNotFoundException 或版本不兼容
provided 范围解决:
- 编译时提供,保证编译通过
- 不打包,避免和容器冲突
适用场景:
| 场景 | 外部提供者 | 说明 |
|---|---|---|
| Servlet API | Tomcat/Jetty | Web 容器内置 |
| JSP API | Tomcat | Web 容器内置 |
| Java EE API | 应用服务器 | GlassFish、WildFly |
| Lombok | IDE | 编译时处理,运行时不需要 |
provided 的特殊用途—— Lombok:
text
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
为什么 Lombok 用 provided:
text
Lombok 工作原理:
- 编译时:Lombok 注解处理器修改源码(生成 getter/setter 等)
- 运行时:生成的代码已存在,Lombok jar 不需要
所以:
- 编译时需要 Lombok jar
- 运行时不需要
- 打包时不应包含
provided 正好满足这个需求
runtime 范围
特点:编译不需要,运行时才需要。
text
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
<scope>runtime</scope>
</dependency>
实际行为:
text
编译时:mysql-connector.jar 不在 classpath
测试时:mysql-connector.jar 在 classpath
运行时:mysql-connector.jar 在 classpath
打包时:mysql-connector.jar 打包进最终产物
为什么编译时不需要:
text
JDBC 代码只使用 JDK 内置接口:
// 代码只用到 java.sql.* 包(JDK 内置)
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
// 编译时只需要 java.sql.* 接口
// 不需要 MySQL 具体实现
// 运行时才加载 MySQL 驱动实现
Class.forName("com.mysql.cj.jdbc.Driver"); // 反射加载
Connection conn = DriverManager.getConnection(url); // 使用实现
适用场景:
| 场景 | 说明 |
|---|---|
| JDBC 驱动 | 代码用 JDBC 接口,运行需具体驱动 |
| 日志实现 | 代码用 SLF4J 接口,运行需 Logback |
| 数据库连接池 | 代码用接口,运行需具体实现 |
runtime vs compile 的选择:
text
问题:MySQL 驱动应该用 compile 还是 runtime?
用 compile:
- 编译时可见(但代码不直接用驱动类)
- 打包时包含
- 没问题,但有些冗余
用 runtime(推荐):
- 编译时不可见(代码只用 JDBC 接口)
- 打包时包含
- 更精确,体现设计意图
如果代码直接引用驱动类:
com.mysql.cj.jdbc.Driver driver = new com.mysql.cj.jdbc.Driver();
则必须用 compile,否则编译失败
test 范围
特点:仅测试阶段可用,不参与生产和编译。
text
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
实际行为:
text
编译时:junit.jar 不在 classpath(主代码不能用 JUnit)
测试编译时:junit.jar 在 classpath(测试代码可用)
测试运行时:junit.jar 在 classpath
运行时:junit.jar 不在 classpath
打包时:junit.jar 不打包
为什么测试代码可以用但主代码不能用:
text
项目结构:
src/main/java/ ← 主代码(编译 classpath)
src/test/java/ ← 测试代码(测试 classpath)
scope=test 时:
- junit.jar 只加入测试 classpath
- 主代码编译时 junit.jar 不在 classpath
- 在 src/main/java 中写 import org.junit.* → 编译失败
- 在 src/test/java 中写 import org.junit.* → 编译成功
这样设计防止:
- 测试代码混入生产代码
- 测试库被打包进生产环境
适用场景:
| 库 | 说明 |
|---|---|
| JUnit | 单元测试框架 |
| Mockito | Mock 测试框架 |
| AssertJ | 断言库 |
| Spring Test | Spring 测试支持 |
| TestContainers | 容器化测试 |
传递性依赖的范围影响
传递规则—— 核心
依赖的 scope 影响传递后的 scope:
| 原依赖 scope | 传递后的 scope | 说明 |
|---|---|---|
| compile | compile | 正常传递 |
| provided | 不传递 | 只在本项目有效 |
| runtime | runtime | 传递但保持 runtime |
| test | 不传递 | 只在本项目有效 |
传递示例详解
text
示例1:compile 传递
项目 A 依赖 B(compile)
B 依赖 C(compile)
结果:A 获得 C(compile)
依赖树:
A
└── B (compile)
└── C (compile)
A 的有效依赖:B (compile) + C (compile)
text
示例2:provided 不传递
项目 A 依赖 B(compile)
B 依赖 servlet-api(provided)
结果:A 不获得 servlet-api
依赖树:
A
└── B (compile)
└── servlet-api (provided) ← 不传递给 A
A 的有效依赖:B (compile)
(没有 servlet-api)
原因:provided 表示"容器提供",只在 B 项目有效
A 项目可能用不同的容器,不应继承 B 的 provided 依赖
text
示例3:test 不传递
项目 A 依赖 B(compile)
B 依赖 junit(test)
结果:A 不获得 junit
依赖树:
A
└── B (compile)
└── junit (test) ← 不传递给 A
A 的有效依赖:B (compile)
(没有 junit)
原因:test 表示"仅测试",只在 B 的测试中用
A 不应继承 B 的测试依赖
传递规则的实际意义
text
场景:你依赖 Spring Boot starter
Spring Boot starter 内部:
- spring-boot-starter-web (compile)
├── spring-webmvc (compile) → 传递给你
├── spring-web (compile) → 传递给你
├── tomcat-embed-core (compile) → 传递给你
└── jackson-databind (compile) → 传递给你
你只需声明一个 starter,自动获得所有依赖(都是 compile)
如果 starter 内部有 provided 依赖:
- spring-boot-starter-web 可能依赖某个 provided 库
- 这个 provided 库不会传递给你
- 你需要自己判断是否需要这个库
scope 选择决策指南
快速决策表
| 问题 | 答案 → 选择 scope |
|---|---|
| 代码是否直接使用这个类? | 否 → runtime 或不引入 |
| 是否只在测试代码中使用? | 是 → test |
| 运行时是否有外部提供? | 是 → provided |
| 以上都不是 | → compile(默认) |
典型项目完整示例
text
<!-- Web 项目依赖配置示例 -->
<dependencies>
<!-- ========== compile 范围 ========== -->
<!-- Spring Web:核心功能,全程需要 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.20</version>
<!-- scope 默认 compile -->
</dependency>
<!-- Jackson:业务代码直接使用 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
<!-- ========== provided 范围 ========== -->
<!-- Servlet API:Tomcat 提供 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- Lombok:编译时处理 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<!-- ========== runtime 范围 ========== -->
<!-- MySQL 驱动:运行时加载 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
<scope>runtime</scope>
</dependency>
<!-- Logback:SLF4J 的实现 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
<scope>runtime</scope>
</dependency>
<!-- ========== test 范围 ========== -->
<!-- JUnit:单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- Mockito:Mock 测试 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.6.1</version>
<scope>test</scope>
</dependency>
</dependencies>
常见问题与解决方案
问题1:测试依赖混入生产代码
text
错误:在 src/main/java 中写测试代码
报错:
[ERROR] package org.junit does not exist
原因:
JUnit scope=test,主代码编译时不在 classpath
解决:
1. 把测试代码移到 src/test/java
2. 或如果必须在主代码用,改 scope 为 compile(不推荐)
问题2:provided 依赖运行时找不到
text
场景:本地运行(没有 Tomcat)
报错:
java.lang.NoClassDefFoundError: javax/servlet/ServletContext
原因:
scope=provided,运行时 classpath 没有 servlet-api
解决:
1. 正常部署到 Tomcat运行(容器提供)
2. 本地调试时临时改 scope 为 compile
3. 或使用嵌入式 Tomcat(spring-boot-starter-tomcat)
问题3:runtime 依赖编译报错
text
错误:直接使用驱动类
代码:
import com.mysql.cj.jdbc.Driver; // 直接引用驱动类
Driver driver = new Driver(); // 编译报错!
报错:
[ERROR] package com.mysql.cj.jdbc does not exist
原因:
MySQL 驱动 scope=runtime,编译时不在 classpath
解决:
1. 改用 JDBC 标准接口(推荐)
Class.forName("com.mysql.cj.jdbc.Driver"); // 反射
2. 或改 scope 为 compile
问题4:打包体积过大
text
问题:war 包 50MB,部署慢
诊断:
mvn dependency:tree | grep "compile"
发现:
- test 依赖错误地设为 compile
- provided 依赖错误地设为 compile
- 不需要的库设为 compile
解决:
检查每个 compile 依赖:
- 是否测试专用?→ 改 test
- 是否容器提供?→ 改 provided
- 是否运行时加载?→ 改 runtime
问题5:依赖传递带来意外库
text
问题:项目引入了意外的依赖
诊断:
mvn dependency:tree
发现:
[INFO] +- org.springframework:spring-webmvc:5.3.20
[INFO] | +- org.springframework:spring-web:5.3.20
[INFO] | +- com.example:unwanted-lib:1.0.0 ← 意外的依赖
原因:
spring-webmvc 传递依赖 unwanted-lib
解决:
1. 用 exclusions 排除
2. 或依赖方修改 scope(provided/test 不传递)
scope 对比速查表
功能对比
| scope | 编译 | 测试 | 运行 | 打包 | 传递 | 典型用途 |
|---|---|---|---|---|---|---|
| compile | ✓ | ✓ | ✓ | ✓ | ✓ | 核心依赖 |
| provided | ✓ | ✓ | ✗ | ✗ | ✗ | 容器提供 |
| runtime | ✗ | ✓ | ✓ | ✓ | ✓ | 驱动实现 |
| test | ✗ | ✓ | ✗ | ✗ | ✗ | 测试框架 |
传递对比
text
A 依赖 B (scope=X),B 依赖 C (scope=Y)
结果:A 获得 C 的 scope
B.compile → C.compile → A 获得 C.compile
B.compile → C.provided → A 不获得 C
B.compile → C.runtime → A 获得 C.runtime
B.compile → C.test → A 不获得 C
B.provided → C.* → A 不获得任何 C
B.test → C.* → A 不获得任何 C
要点总结
- scope 决定可见性:控制依赖在编译、测试、运行阶段是否可用
- compile 默认范围:全程可用,会打包和传递
- provided 用于容器提供:编译可用,不打包,不传递,避免冲突
- runtime 用于运行时加载:编译不可见,运行可用,会打包
- test 用于测试专用:主代码不可用,不打包,不传递
- provided 和 test 不传递:不会传递给下游项目
- 正确选择 scope:避免打包臃肿、运行缺少依赖、版本冲突
- 诊断命令:dependency:tree 查看 scope 和传递关系
📝 发现内容有误?点击此处直接编辑