全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-23 12 分钟 ✍️ juanwangdev

依赖范围详解

依赖范围(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业务代码直接使用
MyBatisORM 框架,运行时必需
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 APITomcat/JettyWeb 容器内置
JSP APITomcatWeb 容器内置
Java EE API应用服务器GlassFish、WildFly
LombokIDE编译时处理,运行时不需要

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单元测试框架
MockitoMock 测试框架
AssertJ断言库
Spring TestSpring 测试支持
TestContainers容器化测试

传递性依赖的范围影响

传递规则—— 核心

依赖的 scope 影响传递后的 scope

原依赖 scope传递后的 scope说明
compilecompile正常传递
provided不传递只在本项目有效
runtimeruntime传递但保持 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

要点总结

  1. scope 决定可见性:控制依赖在编译、测试、运行阶段是否可用
  2. compile 默认范围:全程可用,会打包和传递
  3. provided 用于容器提供:编译可用,不打包,不传递,避免冲突
  4. runtime 用于运行时加载:编译不可见,运行可用,会打包
  5. test 用于测试专用:主代码不可用,不打包,不传递
  6. provided 和 test 不传递:不会传递给下游项目
  7. 正确选择 scope:避免打包臃肿、运行缺少依赖、版本冲突
  8. 诊断命令:dependency:tree 查看 scope 和传递关系

📝 发现内容有误?点击此处直接编辑

← 上一篇 依赖版本冲突解决
下一篇 → 三大生命周期
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库