Maven scope 有哪些类型?什么时候该用 compile、provided 或 runtime?
Maven 的 scope 决定一个依赖在哪些阶段可见:编译、测试、运行、打包,以及是否会传递给下游项目。它不是简单的分类标签,而是在控制 classpath 边界。scope 配错时,轻则最终包变大,重则本地能跑、服务器启动就报类找不到。
最常见的是 compile,它是默认范围,编译、测试、运行都可用,也会传递。provided 表示编译和测试需要,但运行时由 JDK、容器或平台提供。runtime 表示编译时不需要,运行和测试时需要。test 只在测试代码中可见,不会进入正式产物。system 需要指定本地 jar 路径,几乎不推荐。import 只能用于 dependencyManagement,常用来导入 BOM。
xml<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.34</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency>
选择 scope 的判断方法
判断 scope 时可以先问三个问题:编译主代码是否需要它,运行产物是否需要它,下游项目是否应该感知它。三个答案都是“是”,通常就是 compile;只在测试里用,就是 test;编译要用但运行环境提供,就是 provided;编译不直接用但运行要加载,就是 runtime。这个判断比死记六种 scope 更可靠。
打包方式也会影响 scope 的效果。普通 jar 项目不会把依赖直接塞进 jar,Spring Boot 这类可执行包会重新打包依赖,Web 项目生成 war 时又要考虑容器提供的库。比如 servlet-api 在传统 war 中适合 provided,但在嵌入式容器的可执行 jar 场景下,相关依赖可能由 starter 间接带入。边界不是某个依赖永远用某个 scope,而是看运行环境谁负责提供它。
排查 scope 问题时,除了看 POM,还要看最终产物。可以用 mvn dependency:tree 看解析结果,用 jar tf target/app.jar 或查看 WEB-INF/lib 确认依赖是否被打包。很多线上问题不是依赖没声明,而是 scope 让它没有进入最终运行路径。
在多模块项目里,scope 还会影响下游模块的 classpath。一个公共 api 模块如果把实现库设为 compile,下游会被迫继承这个选择;如果设成 provided 或 optional,又可能需要下游自己补依赖。这个边界要在模块设计时想清楚,不能等打包失败后再临时改。对库作者来说,scope 既是构建配置,也是对使用方的一种契约。因此评审 POM 时,scope 应该和依赖版本一样被认真检查。尤其是从 war 迁移到可执行 jar 时,原来合理的 provided 可能就需要重新评估。否则 scope 看起来只是一个小字段,实际却会改变整个运行包的内容。
追问
compile scope 为什么是默认值?
大多数业务代码依赖的库既要参与编译,也要在运行时使用,所以 Maven 把 compile 设为默认值。比如工具类库、核心框架 API、业务 SDK 通常都适合 compile。它的代价是会向下游传递,库项目如果滥用 compile,会把很多不必要的依赖带给使用方。取舍原则是:只有公共 API 暴露出去的类型,才更有理由使用 compile。
provided 适合哪些场景?
provided 适合运行环境已经提供该依赖的情况,例如 Servlet API 由 Tomcat 提供,Lombok 只在编译期生成代码。这样可以避免把容器已有的 jar 再打进应用,减少冲突。边界是你必须确定运行环境真的有这个依赖,而且版本兼容。最常见的坑是本地测试靠 Maven classpath 能跑,部署到精简容器后缺包。
xml<dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency>
runtime 和 compile 怎么区分?
如果代码编译时只依赖标准接口,具体实现到运行时才需要,就适合 runtime。JDBC 驱动是典型例子,业务代码引用的是 java.sql 接口,不需要在编译期直接使用 MySQL 驱动类。这样做能让编译 classpath 更干净,也避免把实现细节误暴露给上游模块。踩坑点是某些框架启动时会反射加载驱动,scope 写成 test 或漏配时,编译没问题但启动失败。
test scope 会不会传递给正式代码?
不会,test 只在测试编译和测试运行阶段可见,不会参与正式打包,也不会传递给依赖你的项目。JUnit、Mockito、AssertJ 这类测试库都应该放 test。取舍点是测试工具不要污染生产运行环境,尤其是 mock、内嵌服务器和测试容器库。常见坑是把测试辅助类放进 src/main/java,导致主代码反过来依赖 test scope,编译直接失败。
import scope 为什么只能配合 dependencyManagement?
import 的作用是导入另一个 POM 的依赖管理配置,本质上是把一组版本约束合并进当前项目。它不是普通 jar 依赖,所以不能放在常规 <dependencies> 中使用。Spring Boot 项目常通过导入 BOM 统一 Spring、日志、Jackson 等版本,避免手动拼版本矩阵。边界是 BOM 只管版本,不保证你的代码兼容升级后的行为,升级前仍然要跑测试和依赖树检查。