Maven 核心机制学习笔记:POM 继承、Profiles、生命周期、依赖作用域与版本

Maven 的核心并不只是“下载依赖和执行 mvn package”,它实际上围绕 POM、构建生命周期、依赖管理、Profile 和版本体系组织整个 Java 工程的构建过程。本文把日常开发中最常用、也最容易混淆的几个点放在一起梳理。

POM 与项目继承

POM(Project Object Model)是 Maven 项目的核心描述文件。Maven 执行构建时,会先读取 pom.xml,得到项目坐标、依赖、插件、仓库以及构建配置。

一个项目可以通过 <parent> 继承父 POM:

1
2
3
4
5
6
<parent>
<groupId>com.example</groupId>
<artifactId>project-parent</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>

父 POM 中大多数配置都可以被子项目继承,常见的包括:

  • groupIdversion
  • descriptionurlinceptionYear
  • organizationlicenses
  • developerscontributorsmailingLists
  • scmissueManagementciManagement
  • properties
  • dependencies
  • dependencyManagement
  • repositoriespluginRepositories
  • build 中的插件执行和插件配置
  • reporting

需要特别注意,artifactIdnameprerequisitesprofiles 本身不会像普通字段那样直接继承。对于父 POM 中已经激活的 Profile,子项目继承的是它产生的实际配置效果,而不是 Profile 定义本身。

实际排查继承结果时,可以直接查看 Maven 最终计算出来的 Effective POM:

1
mvn help:effective-pom

这比只盯着当前模块的 pom.xml 更可靠,因为 Maven 最终使用的是父 POM、Super POM、Profiles 和当前 POM 合并后的结果。

Maven Profiles 与 application.yml

Profile 用来根据不同构建环境修改 POM。例如开发、测试、生产环境使用不同参数时,可以在一个 pom.xml 中定义多套配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<profiles>
<profile>
<id>dev</id>
<properties>
<spring.profile>dev</spring.profile>
<app.log-level>DEBUG</app.log-level>
</properties>
</profile>

<profile>
<id>prod</id>
<properties>
<spring.profile>prod</spring.profile>
<app.log-level>INFO</app.log-level>
</properties>
</profile>
</profiles>

通过 -P 激活:

1
2
3
mvn clean package -Pdev

mvn clean package -Pprod

查看当前实际激活的 Profile:

1
mvn help:active-profiles

查看某个 Profile 生效后的完整 POM:

1
mvn help:effective-pom -Pdev

Profile 中的值如何给 application.yml 使用

如果项目继承了 spring-boot-starter-parent,Spring Boot 已经为 application.propertiesapplication.yml 提供了合适的 Maven Resource Filtering 配置。

由于 Spring 自己使用 ${...} 作为属性占位符,为避免和 Maven 占位符冲突,Spring Boot 默认建议 Maven 过滤时使用 @...@

例如 POM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<profiles>
<profile>
<id>dev</id>
<properties>
<spring.profile>dev</spring.profile>
<app.log-level>DEBUG</app.log-level>
</properties>
</profile>

<profile>
<id>prod</id>
<properties>
<spring.profile>prod</spring.profile>
<app.log-level>INFO</app.log-level>
</properties>
</profile>
</profiles>

application.yml 中可以这样取:

1
2
3
4
5
6
7
spring:
profiles:
active: @spring.profile@

logging:
level:
root: @app.log-level@

执行:

1
mvn clean package -Pdev

process-resources 阶段,Maven 会处理 src/main/resources,最终生成到 target/classes/application.yml 中的内容已经被替换,例如:

1
2
3
4
5
6
7
spring:
profiles:
active: dev

logging:
level:
root: DEBUG

这里要区分两件事:

  • Maven Profile 是构建期配置-Pdev 决定这次构建怎么生成产物。
  • Spring Profile 是应用运行期配置,也可以通过 --spring.profiles.active=dev、环境变量等方式决定。

如果希望“同一个 JAR 在 dev、test、prod 环境直接复用”,通常更适合把环境选择留到 Spring Boot 运行时,而不是使用 Maven Profile 把环境值永久写进构建产物。

Maven 默认构建生命周期

Maven 内置 defaultcleansite 三套生命周期。其中日常使用最多的是 default 生命周期。

完整的 default 生命周期主要阶段按顺序为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
validate
-> initialize
-> generate-sources
-> process-sources
-> generate-resources
-> process-resources
-> compile
-> process-classes
-> generate-test-sources
-> process-test-sources
-> generate-test-resources
-> process-test-resources
-> test-compile
-> process-test-classes
-> test
-> prepare-package
-> package
-> pre-integration-test
-> integration-test
-> post-integration-test
-> verify
-> install
-> deploy

开发过程中最常接触的是下面几个阶段:

Phase 作用
validate 校验项目结构和必要信息是否完整
compile 编译主代码
test 执行单元测试
package 将项目打包为 JAR、WAR 等产物
verify 对构建和集成测试结果进行进一步校验
install 将产物安装到本地 Maven 仓库
deploy 将产物发布到远程 Maven 仓库

Maven 执行某一个阶段时,会自动执行它之前的所有阶段。

例如:

1
mvn package

并不是只执行 package,而是从 validate 一直执行到 package

同理:

1
mvn install

会执行到 install,因此前面的编译、测试、打包等阶段也都会执行。

Maven 依赖作用域

依赖作用域(Dependency Scope)主要解决两个问题:

  1. 一个依赖应该出现在哪些 Classpath 中。
  2. 一个依赖是否继续向下传递为传递依赖。

Maven 官方定义了 6 种 Scope。

compile

默认作用域,不写 <scope> 时就是 compile

1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>

它会出现在编译、测试和运行 Classpath 中,并且可以作为传递依赖继续传播给依赖当前项目的其他项目。

provided

provided 表示:编译项目时需要,但是正式运行环境预计由 JDK、Servlet 容器或其他外部运行环境提供。

典型场景是传统 WAR 项目的 Servlet API:

1
2
3
4
5
6
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>

它会进入编译和测试 Classpath,但不会进入 Maven 的运行时 Classpath,也不会作为普通传递依赖传播。

这里有一个常见误解:provided 不是“编译和运行都由项目提供”,而是“编译时项目需要,运行时由外部环境提供”。

runtime

runtime 表示编译主代码时不需要,但程序真正运行时需要。

典型场景是 JDBC 驱动:

1
2
3
4
5
6
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.8</version>
<scope>runtime</scope>
</dependency>

业务代码通常面向 JDBC API 编程,真正的数据库驱动实现由运行时加载,因此驱动不需要进入主代码的编译 Classpath。

runtime 会进入运行和测试 Classpath。

test

只用于测试代码的编译和运行,不参与正式应用运行,也不会传递给依赖当前项目的其他项目。

1
2
3
4
5
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

system

systemprovided 有些相似,但 Maven 不会从仓库解析这个依赖,而是要求显式指定本机 JAR 路径。

1
2
3
4
5
6
7
<dependency>
<groupId>com.example</groupId>
<artifactId>legacy-sdk</artifactId>
<version>1.0.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/legacy-sdk.jar</systemPath>
</dependency>

这种方式会让构建依赖本机目录结构,破坏构建可移植性,因此 Maven 官方并不推荐。更合适的方式通常是把内部 JAR 发布到公司的 Maven 私服。

import

import 比较特殊,它只允许:

  • typepom
  • 位于 <dependencyManagement>

它的主要用途就是导入 BOM:

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.5.6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

这并不是把一个普通依赖放到 Classpath,而是把目标 POM 中 dependencyManagement 管理的依赖版本导入当前项目。

Scope 对照

Scope 编译 测试 运行 传递性 常见用途
compile Y Y Y Y 普通业务依赖
provided Y Y N N Servlet API、容器提供的 API
runtime N Y Y Y JDBC 驱动等运行时实现
test N Y N N JUnit、Mockito
system Y Y 取决于本机显式路径 N 本地遗留 JAR,不推荐
import - - - - dependencyManagement 中导入 BOM

需要注意,Scope 本质上控制的是 Classpath 和依赖传递关系,不能简单等同于“最终 JAR 中有没有这个依赖”。普通 Maven JAR 默认并不会把所有依赖直接塞进 JAR;Spring Boot Fat JAR、WAR 等具体打包行为还取决于对应插件和 Packaging 类型。

工程版本号约定

Maven 使用 groupId:artifactId:version 唯一确定一个构件,其中 version 本身并不强制要求必须使用某一种版本模型。

实际项目中最常见的是类似语义化版本的形式:

1
<major>.<minor>.<patch>[-qualifier]

例如:

1
2
3
4
5
1.0.0
2.3.2
3.5.42
1.0.0-M1
1.0.0-RC1

通常可以这样理解:

  • major:不兼容的大版本变更。
  • minor:向后兼容的功能增加。
  • patch:缺陷修复和较小改动。
  • M1RC1 等 qualifier:里程碑版本、候选发布版本等预发布标记。

旧版 Spring 项目中经常能看到:

1
5.1.9.RELEASE

这种写法属于历史上的项目版本命名习惯,并不是 Maven 强制规定。当前 Maven 官方版本命名建议中,也更推荐正式发布版本直接使用:

1
5.1.9

而不是额外增加 RELEASE qualifier。

SNAPSHOT 与 Release

开发阶段常见:

1
<version>1.0.0-SNAPSHOT</version>

SNAPSHOT 表示当前版本仍然处于开发过程中,它对应的是某个时间点的开发构建,同一个 1.0.0-SNAPSHOT 后续可以继续被新的构件覆盖或更新。

例如:

1
1.0.0-SNAPSHOT

最终发布后通常变成:

1
1.0.0

随后项目进入下一个开发周期:

1
1.0.1-SNAPSHOT

因此可以把它理解为:

1
2
1.0.0-SNAPSHOT  ->  1.0.0  ->  1.0.1-SNAPSHOT
开发版 正式版 下一开发版

Maven 对 -SNAPSHOT 有专门的仓库处理逻辑,Snapshot 仓库和 Release 仓库也可以分别配置更新策略。

总结

理解 Maven 时,可以把几个概念串成一条线:

1
2
3
4
5
6
7
POM
-> 父子 POM 继承公共配置
-> Profile 根据构建环境修改配置
-> process-resources 处理 application.yml 等资源
-> 生命周期决定 compile/test/package/install/deploy 的执行顺序
-> dependency scope 决定依赖进入哪些 Classpath
-> version + SNAPSHOT/Release 管理构件的开发和发布状态

日常排查 Maven 问题时,下面几条命令非常实用:

1
2
3
4
5
6
7
8
9
10
11
# 查看最终生效的 POM
mvn help:effective-pom

# 查看激活的 Profiles
mvn help:active-profiles

# 查看依赖树
mvn dependency:tree

# 构建指定环境
mvn clean package -Pdev

参考资料


Maven 核心机制学习笔记:POM 继承、Profiles、生命周期、依赖作用域与版本
https://allendericdalexander.github.io/2026/09/09/java/maven-core-notes-hexo/
作者
AtLuoFu
发布于
2026年9月9日
许可协议