Maven 的生命周期和依赖 scope 分别是什么?
简化版
Maven 生命周期是一组有固定顺序的构建阶段(phase),执行某个 phase 会连同它之前的所有 phase 一起执行(如 mvn package 会先跑 validate→compile→test 再到 package)。真正干活的不是 phase 本身,而是绑定到 phase 上的插件目标(plugin goal)。依赖 scope 决定一个依赖在编译、测试、运行、打包各阶段是否可见、是否传递给下游。面试重点是分清 compile(全程可见并传递)、provided(编译测试可见、运行由容器提供)、runtime(编译不可见、运行测试可见)、test(只测试可见) 的边界。
详细版
三套生命周期 + default 常用 phase:
Maven 有 clean、default、site 三套独立生命周期。最常问的是 default 的关键 phase(有序):
validate → compile → test → package → verify → install → deploy
| phase | 作用 |
|---|---|
compile | 编译主代码(src/main/java) |
test | 编译并运行单元测试(Surefire 插件) |
package | 打包成 jar / war |
verify | 运行集成测试 + 质量校验(Failsafe 等) |
install | 把产物装到本地仓库(供本机其他项目引用) |
deploy | 发布到远程仓库(供团队/环境使用) |
三层概念别混:lifecycle(路线)→ phase(站点)→ goal(具体动作)。mvn package 跑到 package 站点,途中每站执行绑定的 goal(如 maven-compiler-plugin:compile)。
四种核心 scope 对比:
| scope | 编译 classpath | 测试 classpath | 运行 classpath | 传递给下游 | 典型例子 |
|---|---|---|---|---|---|
compile(默认) | ✅ | ✅ | ✅ | ✅ | 大多数业务库 |
provided | ✅ | ✅ | ❌(容器提供) | ❌ | Servlet API、Lombok |
runtime | ❌ | ✅ | ✅ | ✅ | JDBC 驱动 |
test | ❌ | ✅ | ❌ | ❌ | JUnit、Mockito |
system | ✅ | ✅ | ✅ | ❌ | 本地 jar(应避免) |
完整版教学
一、生命周期解决什么问题
Maven 把一次构建拆成固定顺序的稳定阶段,目的是让所有项目不用各写各的构建脚本——无论项目大小,「编译、测试、打包、安装、发布」都按统一顺序发生,插件只需挂到合适的 phase 即可。
面试里最容易犯的错是把 lifecycle、phase、goal 混成一个概念。正确理解:
- lifecycle(生命周期):一条路线(default / clean / site)。
- phase(阶段):路线上的站点,有固定顺序。
- goal(插件目标):真正执行的动作,绑定在某个 phase 上。
执行 phase = 从头跑到这个站点,途中每站执行绑定的 goal。
| 概念 | 类比 | 示例 |
|---|---|---|
| lifecycle | 路线 | default |
| phase | 站点 | compile、test、package |
| goal | 具体动作 | compiler:compile、surefire:test |
Maven 构建要按“路线、站点、动作”三层记:你执行的是 phase,真正干活的是绑定到 phase 上的 goal。
二、phase 和 goal 的关系(关键)
执行 mvn test 时,Maven 按 default 生命周期从 validate 一路跑到 test 阶段;但 test 阶段能不能真的运行测试,取决于是否有 Surefire 插件的 goal 绑定在这里(Maven 默认给 Java 项目绑好了)。
对比两种命令:
mvn test # 跑完整生命周期到 test(会先 compile 等前置步骤)
mvn compiler:compile # 直接调用某个插件 goal,不跑完整生命周期
mvn compiler:compile 只执行单个 goal,不会帮你跑前置阶段。实际项目里更常用 phase(如 mvn package),因为它能把前置步骤一起带上,一条命令完成完整构建。
mvn package
validate -> compile -> test -> package
mvn compiler:compile
只执行 compiler 插件的 compile goal
三、package / install / deploy 的区别(高频)
三者是 default 生命周期后段的关键站点,区别常被追问:
package:把编译产物打成 jar / war,产物在target/目录。install:在 package 之后,把产物安装到本地仓库(~/.m2/repository)——这样本机的其他项目能通过依赖坐标引用它。多模块本地开发常用。deploy:把产物发布到远程仓库(Nexus、私服、中央仓库)——供团队其他成员或部署环境使用,通常在 CI 里执行。
说清「package 打包 → install 装本地仓库 → deploy 发远程仓库」这条递进,基本就答到核心了。
四、scope 影响的不只是打包
很多人以为 scope 只影响「打不打进包」,其实 scope 同时影响四件事:编译 classpath、测试 classpath、运行 classpath、以及是否传递给下游。逐个看四种核心 scope:
compile(默认):全程可见(编译/测试/运行都在)且会传递给依赖你的项目。大多数业务库用它。provided:编译和测试可见,但运行时由容器/环境提供,不打进包、不传递。典型是 Servlet API(Tomcat 会提供,你不该把它打进 war),还有 Lombok(只在编译期生成代码)。runtime:编译时不可见,运行和测试时可见。典型是 JDBC 驱动——你的代码只依赖 JDBC 的接口(java.sql.*,JDK 自带),编译时不需要具体驱动,只有运行时才需要 MySQL/PostgreSQL 的驱动实现。设成 runtime 能防止代码里误用具体驱动的类。test:只在测试 classpath 可见,不传递、不打进生产包。JUnit、Mockito 必须是 test,否则生产代码会携带测试库、增大体积、甚至引入风险。
五、scope 与依赖传递的常见坑
compile依赖会传递给下游(依赖你的项目也能用到)。test依赖不传递(别人依赖你时用不到你的测试库)。provided也不会被带到运行环境——它假设「运行时环境会提供」。
最常见的事故:「本地能跑,服务器报 ClassNotFoundException」——根因往往是把运行必需的依赖误设成 provided,或者「容器会提供这个依赖」这件事在不同环境并不成立(本地容器有、生产容器版本不对或没有)。设 provided 前一定要确认运行环境真的会提供。
六、排查构建问题的顺序
构建出问题时,按这个顺序查:
- 看执行的是哪个 phase —— 确认跑到哪一步失败。
- 看相关插件 goal 有没有绑定和配置 —— 是不是插件没配好。
mvn dependency:tree—— 看依赖版本和来源。mvn help:effective-pom—— 看继承、profile、插件管理合并后的最终配置。
关键认知:不要只盯着 pom 里表面那几行——Maven 的真实输入是 effective POM(父 pom + BOM + profile 合并后的结果),很多「配置没生效」的问题只有看 effective POM 才能发现。
七、工程实践里的选择
- 应用项目关注可重复构建:固定插件版本(不写版本会用默认版本、不同机器可能不同)、用 Maven Wrapper(mvnw) 统一 Maven 版本、CI 里执行
mvn verify(跑到 verify,包含集成测试和质量校验)。 - 库项目还要关注发布元数据、源码包、签名、二进制兼容性。
- 多模块项目:父 pom 用
dependencyManagement/pluginManagement管版本和插件配置,子模块只声明自己实际需要的依赖(见「dependencyManagement vs dependencies」专题),边界清晰才不失控。
八、常见误区与追问
- 误区:phase 本身负责执行编译测试。 phase 只是生命周期中的阶段,真正执行动作的是绑定到该阶段的插件 goal。
- 误区:
mvn compiler:compile等同于mvn compile。 前者直接调用单个 goal,后者跑生命周期到 compile 阶段,会包含前置阶段。 - 误区:scope 只影响是否打进包。 scope 同时影响编译、测试、运行 classpath 以及是否传递给下游。
- 追问:
package、install、deploy最大区别是什么?package打到 target,install装到本地仓库,deploy发布到远程仓库。 - 追问:为什么 JDBC 驱动常用
runtime? 代码编译依赖 JDBC 接口,具体驱动只在运行时由 DriverManager 加载。 - 追问:
provided最容易造成什么线上问题? 运行环境没提供该依赖或版本不对时,会出现ClassNotFoundException或行为不一致。
九、加强记忆
Maven 三层关系:lifecycle(路线,default/clean/site)→ phase(站点,有序)→ goal(插件目标,真正干活);执行 phase = 从头跑到该站、途中执行绑定的 goal。default 关键 phase:validate→compile→test→package→verify→install→deploy,其中 package 打包、install 装本地仓库、deploy 发远程仓库。scope 决定依赖在编译/测试/运行/传递中的可见性:compile(全程可见并传递)、provided(编译测试可见、运行靠容器、不传递,如 Servlet API/Lombok)、runtime(编译不可见、运行测试可见,如 JDBC 驱动)、test(只测试可见,如 JUnit)。坑:把运行必需依赖误设 provided → 本地能跑服务器 ClassNotFound。排查看 dependency:tree + effective-pom(真实输入是 effective POM)。