乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 介绍与核心概念

    • Maven是什么
    • 约定优于配置
  • 第2章 安装与配置

    • 安装与验证
    • settings.xml
    • 本地仓库与镜像
  • 第3章 POM与项目坐标

    • POM
    • GAV坐标
    • packaging
  • 第4章 标准目录布局

    • 标准目录布局
  • 第5章 依赖机制

    • dependencies
    • scope
    • 依赖传递
    • 依赖冲突与调解
    • exclusions
    • optional
    • dependencyManagement
  • 第6章 仓库

    • 仓库体系
    • 本地仓库
    • 远程仓库与镜像
    • 私服
  • 第7章 构建生命周期

    • 生命周期概述
    • clean 生命周期
    • default 生命周期
    • site 生命周期
    • 生命周期与插件绑定
  • 第8章 插件

    • 插件概述
    • maven-compiler-plugin
    • maven-surefire-plugin
    • maven-war-plugin
  • 第9章 继承与聚合

    • parent继承
    • 聚合
    • BOM
    • properties
  • 第10章 属性与资源过滤

    • 资源过滤
    • Profile
  • 第11章 常用命令

    • mvn compile
    • mvn test
    • mvn package
    • mvn clean
    • mvn install
    • mvn dependency:tree
  • 第12章 常见问题与最佳实践

    • 依赖冲突排查
    • 最佳实践

scope

本章承接"依赖传递",深入讲解 Maven 的依赖范围机制。理解 scope,是控制依赖"在何时、何地、以何种方式生效"的核心,也是避免测试库污染生产环境、运行时库混入编译期的关键。


核心机制

scope 定义了依赖在项目的类路径(classpath)中的可见性,以及该依赖是否参与传递和参与打包。

这句话包含三个维度:

  1. 何时可见:编译时?测试时?运行时?
  2. 是否传递:依赖你的项目的人,会不会继承这个依赖?
  3. 是否打包:最终产物(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 会导致三个问题:

  1. 打包膨胀:测试库、日志实现、本地工具被打包进产物
  2. 传递污染:你的项目被其他项目依赖时,测试库会传递过去
  3. 编译耦合: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 如何选择"——这是传递依赖带来的典型副作用。

上一页
dependencies
下一页
依赖传递