scope
本章承接"依赖传递",深入讲解 Maven 的依赖范围机制。理解
scope,是控制依赖"在何时、何地、以何种方式生效"的核心,也是避免测试库污染生产环境、运行时库混入编译期的关键。
核心机制
scope 定义了依赖在项目的类路径(classpath)中的可见性,以及该依赖是否参与传递和参与打包。
这句话包含三个维度:
- 何时可见:编译时?测试时?运行时?
- 是否传递:依赖你的项目的人,会不会继承这个依赖?
- 是否打包:最终产物(JAR/WAR)里,包不包含这个依赖?
五种 scope 详解
Maven 定义了五种依赖范围,每种对应不同的生命周期阶段:
compile(默认)
- 编译:可见 ✅
- 测试:可见 ✅
- 运行:可见 ✅
- 打包:包含 ✅
- 传递:会传递 ✅
- 典型依赖:Spring、MyBatis、Guava 等核心框架
不声明 scope 时,默认就是 compile。这是最常见的范围,表示"项目全程都需要这个库"。
provided
- 编译:可见 ✅
- 测试:可见 ✅
- 运行:由容器提供,不打包 ❌
- 打包:不包含 ❌
- 传递:不传递 ❌
- 典型依赖:Servlet API、Lombok(注解处理器)
provided 的含义是"编译和测试时需要,但运行时由外部环境提供"。例如 Web 应用依赖 javax.servlet-api,但部署时 Tomcat 已经自带了这个 JAR,不需要打包进去。
runtime
- 编译:不可见 ❌
- 测试:可见 ✅
- 运行:可见 ✅
- 打包:包含 ✅
- 传递:会传递 ✅
- 典型依赖:JDBC 驱动(如 MySQL Connector)、日志实现(如 Logback)
runtime 表示"编译时不需要,运行时才需要"。例如你的代码通过 DriverManager 动态加载 MySQL 驱动,编译时只需要 JDBC 接口(在 JDK 中),不需要具体的驱动类。
test
- 编译:不可见 ❌
- 测试:可见 ✅
- 运行:不可见 ❌
- 打包:不包含 ❌
- 传递:不传递 ❌
- 典型依赖:JUnit、Mockito、Spring Test
test 将依赖严格限制在测试阶段,确保测试框架不会污染生产代码。这是隔离测试环境的核心机制。
system
- 编译:可见 ✅
- 测试:可见 ✅
- 运行:可见 ✅
- 打包:包含 ✅(需配合
systemPath) - 传递:不传递 ❌
- 典型依赖:本地专有 SDK、遗留系统的 JAR
system 是最特殊的范围,它要求你通过 <systemPath> 指定本地文件系统路径,而不是从 Maven 仓库下载。这意味着构建不可移植——换一台机器,如果路径不同,构建就失败。官方强烈不推荐,仅在无法上传到仓库的遗留系统场景下使用。
五种 scope 对比表
| scope | 编译 classpath | 测试 classpath | 运行 classpath | 参与打包 | 参与传递 | 典型示例 |
|---|---|---|---|---|---|---|
compile | ✅ | ✅ | ✅ | ✅ | ✅ | spring-context |
provided | ✅ | ✅ | ❌(容器提供) | ❌ | ❌ | javax.servlet-api |
runtime | ❌ | ✅ | ✅ | ✅ | ✅ | mysql-connector-java |
test | ❌ | ✅ | ❌ | ❌ | ❌ | junit-jupiter |
system | ✅ | ✅ | ✅ | ✅ | ❌ | 本地 legacy-sdk.jar |
各 scope 的典型依赖示例
<dependencies>
<!-- compile:全程需要,默认不写 scope -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.21</version>
</dependency>
<!-- provided:编译需要,运行由容器提供 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- runtime:编译不需要,运行需要 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
<scope>runtime</scope>
</dependency>
<!-- test:仅测试需要 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<!-- system:本地路径,不推荐 -->
<dependency>
<groupId>com.legacy</groupId>
<artifactId>internal-sdk</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/legacy-sdk.jar</systemPath>
</dependency>
</dependencies>
生活类比:不同场合的着装要求
想象你参加飞翔科技的不同活动:
compile(正装):公司年会、客户拜访——全程需要,代表公司形象。走到哪里都要穿。provided(工牌):进入办公楼需要刷卡,但工牌是公司发的,不是你从家里带的。你每天"使用"它,但不需要把它装进行李箱(打包)带去出差。runtime(雨具):晴天出门不需要带伞(编译时不需要),但天气预报说下午有雨,你把它放在包里备用(运行时可能需要)。test(运动服):公司健身房里的运动服,只在健身时穿(测试时),见客户时绝对不能穿(生产环境不可见)。system(祖传玉佩):你家传的玉佩,只在你家抽屉里(本地路径)。换了个城市,抽屉位置变了,你就找不到它了——不可移植。
图示
上图展示了四种主要 scope 的生命周期覆盖范围。compile 贯穿全程(绿色),provided 止步于打包(橙色),runtime 跳过编译(蓝色),test 严格限制在测试阶段(红色)。system 的行为类似 compile,但带有本地路径的不可移植性。
完整示例
场景
飞翔科技的 employee-system 是一个 Web 应用,需要连接 MySQL 数据库。CTO 大翔要求:
- 编译时不能依赖具体的数据库驱动(方便以后换数据库)
- 测试时使用 H2 内存数据库
- Servlet API 由 Tomcat 提供,不要打包进 WAR
- 单元测试框架不能出现在生产环境
架构师白歌指导后端小崔配置 scope,运维李眉验证打包产物的内容。
操作前:scope 混乱的 POM
小崔最初的 pom.xml 没有区分 scope:
<dependencies>
<!-- ❌ 没写 scope,默认 compile,会打包进 WAR -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<!-- ❌ 没写 scope,默认 compile,编译时可见 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!-- ❌ 没写 scope,默认 compile,会打包进 WAR -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
</dependency>
<!-- ❌ 没写 scope,默认 compile,会打包进 WAR -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
</dependency>
</dependencies>
执行打包:
mvn package
问题:
- WAR 文件里包含了
javax.servlet-api.jar,和 Tomcat 自带的冲突,启动报错ClassCastException h2.jar和junit-jupiter.jar被打包进 WAR,增加了 5MB 体积,且 H2 是测试数据库,不应该出现在生产环境- 业务代码里可以直接
import com.mysql.cj.jdbc.Driver,换数据库时需要改代码
操作后:精确配置 scope
白歌指导小崔修正 pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.feixiang</groupId>
<artifactId>employee-system</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<dependencies>
<!-- compile:核心框架,全程需要 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.21</version>
</dependency>
<!-- provided:编译需要,Tomcat 自带,不打包 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- runtime:编译不需要,运行时需要,打包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
<scope>runtime</scope>
</dependency>
<!-- test:仅测试需要,不打包,不传递 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<scope>test</scope>
</dependency>
<!-- test:测试框架,不打包 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
执行打包并检查内容:
mvn package
jar tf target/employee-system-1.0.0.war | grep "WEB-INF/lib"
输出:
WEB-INF/lib/spring-context-5.3.21.jar
WEB-INF/lib/spring-core-5.3.21.jar
WEB-INF/lib/spring-beans-5.3.21.jar
WEB-INF/lib/mysql-connector-java-8.0.30.jar
# ❌ javax.servlet-api 不在列表中(provided)
# ❌ h2-2.1.214.jar 不在列表中(test)
# ❌ junit-jupiter-5.8.2.jar 不在列表中(test)
变化分析:
javax.servlet-api被标记为provided,WAR 里不再包含,Tomcat 启动正常mysql-connector-java被标记为runtime,编译时不可见(防止代码直接引用 MySQL 专有类),但运行和打包正常h2和junit-jupiter被标记为test,WAR 体积减少,生产环境干净- 李眉的部署脚本无需修改,因为
mvn package的输出已经是"正确"的 WAR
易错点与常见问题
误区一:所有依赖都应该用 compile
错误认知:"我不确定该用什么 scope,就用默认的 compile,反正能用。"
纠正:滥用 compile 会导致三个问题:
- 打包膨胀:测试库、日志实现、本地工具被打包进产物
- 传递污染:你的项目被其他项目依赖时,测试库会传递过去
- 编译耦合:
runtime依赖被错误标记为compile,导致业务代码直接引用具体实现类,换实现时需要改代码
正确做法:根据"编译是否需要直接引用"和"运行是否需要"两个维度判断。
误区二:provided 等于"不需要"
错误认知:"provided 的依赖不打包,所以我不需要关心它。"
纠正:provided 在编译和测试时完全可见。如果你漏声明 javax.servlet-api,编译 HttpServlet 子类时会报错。provided 的含义是"运行时由别人提供",不是"我不需要"。
误区三:system 是"本地依赖的正规方式"
错误认知:"我有一个本地 JAR,用 system scope 是 Maven 推荐的做法。"
纠正:system 是 Maven 的逃生舱,不是正规通道。它破坏了 Maven 仓库体系的核心价值——可移植构建。正确的做法是将本地 JAR 安装到本地仓库(mvn install:install-file)或部署到私有 Nexus/Artifactory,然后用正常的 compile scope 引用。
小结
scope 是控制依赖生命周期边界的核心工具。compile 贯穿全程,provided 由容器提供,runtime 运行时才需要,test 严格隔离在测试阶段,system 是本地路径的临时方案。正确选择 scope,能避免测试库污染生产环境、减少打包体积、降低编译期耦合。
本章与全局的关系:本章解释了"依赖在何时生效"。下一章"依赖冲突与调解"将讲解"当两个依赖带来同一个库的不同版本时,Maven 如何选择"——这是传递依赖带来的典型副作用。