【SpringBoot】Maven 版本管理与 flatten-maven-plugin 插件的使用及分析

第一章. 背景:Maven 多模块项目的版本管理困境

在微服务架构盛行的当下,使用 Maven 构建多模块项目已成为业界标准实践。 项目通常被划分为多个职责单一的子模块,如 xxx-clientxxx-adapterxxx-app 等。 这种结构在提升代码复用性和职责分离度的同时,也引入了版本管理的复杂性。

在快速迭代的开发模式中,项目版本频繁更新司空见惯。若采用传统的手动管理方式,每次发布新版本时,开发人员都需要逐一修改父 pom.xml 和所有子模块 pom.xml 中的版本号。这种全局搜索并替换的操作不仅效率低下、操作繁琐,而且极易因人为疏忽导致版本不一致,从而引发构建失败或依赖冲突等严重问题。因此,寻求一种集中、高效、自动化的版本管理方案势在必行。

第二章. CI/CD 友好的版本占位符

为了应对上述挑战,自 Maven 3.5.0-beta-1 版本起,引入了对持续集成(CI/CD)更为友好的版本占位符:${revision}${sha1}${changelist}

2.1. 核心占位符 ${revision}

${revision} 是最核心的占位符,它允许我们在父模块中通过属性(properties)统一定义整个项目的版本号。

父 POM 配置示例:

1
2
3
4
5
6
7
8
9
10
11
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.pointer.pay</groupId>
<artifactId>pointer-pay-parent</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>

<properties>
<revision>1.0.0-SNAPSHOT</revision>
</properties>
</project>

子模块 POM 配置示例:

子模块通过继承父 POM,其版本号无需再硬编码,Maven 会自动解析为父模块中定义的 ${revision} 值。

1
2
3
4
5
6
7
8
9
10
11
<project>
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.pointer.pay</groupId>
<artifactId>pointer-pay-parent</artifactId>
<version>${revision}</version>
</parent>

<artifactId>pointer-pay-app</artifactId>
</project>

2.2. 增强占位符 ${sha1}${changelist}

除了 ${revision},我们还可以结合 ${sha1}(通常用于 Git commit hash)和 ${changelist}(如 -SNAPSHOT, -RELEASE)来构成更动态、信息更丰富的版本号。

组合使用示例:

1
2
3
4
5
6
7
8
9
<project>
<version>${revision}${sha1}${changelist}</version>

<properties>
<revision>1.0</revision>
<changelist>-SNAPSHOT</changelist>
<sha1/>
</properties>
</project>

在构建时,可以通过命令行动态传入这些变量,实现版本号的灵活控制:

1
mvn -Drevision=2.7.8 -Dchangelist=-RELEASE -Dsha1=ssbd clean package

2.3. 版本占位符的固有缺陷

尽管版本占位符实现了版本号的集中管理,但它存在一个致命缺陷:在执行 mvn installmvn deploy 命令时,Maven 不会自动解析这些占位符。 这导致安装或部署到仓库的构件 pom.xml 文件中,版本号依然是 ${revision} 而非其实际值(如 1.0.0-SNAPSHOT)。

当其他项目依赖这个构件时,Maven 将无法识别 ${revision} 这样的版本号,导致依赖解析失败。 要解决这一问题,必须借助 flatten-maven-plugin 插件。

第三章. 解决方案:flatten-maven-plugin

flatten-maven-plugin 是一个专门用于优化和“扁平化”项目 POM 文件的 Maven 插件。 它的核心功能是在构建过程中生成一个解析了所有变量和继承关系的 .flattened-pom.xml 文件,并用它来替代原始的 pom.xml 进行打包、安装和部署。

3.1. 插件集成与配置

在父模块的 pom.xml 中引入 flatten-maven-plugin 并进行配置是解决问题的关键。

核心配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<build>
<plugins>
<!-- 引入 flatten-maven-plugin 插件 -->
<plugin>
<!-- 插件的组织ID -->
<groupId>org.codehaus.mojo</groupId>
<!-- 插件的构件ID -->
<artifactId>flatten-maven-plugin</artifactId>
<!-- 插件的版本号 -->
<version>1.2.7</version>

<!-- 插件的具体配置项 -->
<configuration>
<!--
关键配置:是否用生成的“扁平化”POM(.flattened-pom.xml)替换项目原始的POM文件。
设置为 'true' 后,在执行 package、install、deploy 等操作时,
Maven会使用已解析版本占位符的扁平化POM,从而确保部署到仓库的POM文件版本号是正确的。
这对于父模块(packaging为pom)尤其重要。
-->
<updatePomFile>true</updatePomFile>
<!--
设置扁平化的模式。'resolveCiFriendliesOnly'是一个非常重要的模式,
它意味着插件只会解析CI友好型占位符(如 ${revision}, ${sha1}, ${changelist}),
而不会移除POM中的其他元素(如 <parent>, <dependencyManagement>, <properties> 等),
从而在解决版本号问题的同时,最大程度地保留了原始POM的结构和信息。
-->
<flattenMode>resolveCiFriendliesOnly</flattenMode>
</configuration>

<!-- 定义插件目标的执行时机 -->
<executions>
<!-- 第一个执行任务:在构建过程中生成扁平化POM -->
<execution>
<!-- 为这个执行任务指定一个唯一的ID -->
<id>flatten</id>
<!-- 绑定到Maven生命周期的 'process-resources' 阶段 -->
<!-- 这确保在打包(package)之前,POM文件就已经被处理完毕 -->
<phase>process-resources</phase>
<!-- 指定要执行的目标(goals) -->
<goals>
<!-- 执行 'flatten' 目标,即生成 .flattened-pom.xml 文件 -->
<goal>flatten</goal>
</goals>
</execution>

<!-- 第二个执行任务:在清理项目时删除生成的扁平化POM -->
<execution>
<!-- 为这个执行任务指定一个唯一的ID -->
<id>flatten-clean</id>
<!-- 绑定到Maven生命周期的 'clean' 阶段 -->
<!-- 当执行 mvn clean 命令时,会触发此任务 -->
<phase>clean</phase>
<goals>
<!-- 执行 'clean' 目标,即删除上次构建生成的 .flattened-pom.xml 文件 -->
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

3.2. 关键配置参数详解

3.2.1. <flattenMode>

flattenMode 用于定义 POM 扁平化的策略。 resolveCiFriendliesOnly 是专门为解决版本占位符问题而设计的模式。 在此模式下,插件只会解析 ${revision}${sha1}${changelist} 这几个变量,而 POM 文件的其余部分则保持原样,避免了过度扁平化带来的信息丢失。

3.2.2. <updatePomFile>

该参数控制是否在构建过程中将生成的 .flattened-pom.xml 设置为项目当前的 POM 文件。

  • 默认行为:默认情况下,只有 packaging 类型不为 pom 的模块(即子模块)才会使用扁平化的 POM。父模块的占位符不会被替换。
  • 设置为 true:将 updatePomFile 显式设置为 true,可以强制所有模块(包括 packagingpom 的父模块)都使用 .flattened-pom.xml。 这是确保父子模块版本号都能被正确解析并部署到仓库的关键。

第四章. 改造多模块项目

下面以一个名为 pointer-pay 的多模块项目为例,演示如何从传统的版本管理方式迁移到使用 ${revision}flatten-maven-plugin 的现代化方案。

4.1. 原始状态:版本号硬编码

改造前,父模块和所有子模块的 pom.xml 中都硬编码了具体的版本号,例如 1.0.0-SNAPSHOT

父 POM (改造前):

1
2
3
4
<groupId>com.pointer.pay</groupId>
<artifactId>pointer-pay</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>

子模块 POM (改造前):

1
2
3
4
5
6
<parent>
<groupId>com.pointer.pay</groupId>
<artifactId>pointer-pay</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>pointer-pay-adapter</artifactId>

4.2. 第一步:引入版本占位符

修改父 POM,使用 ${revision} 占位符,并在 <properties> 区域定义版本号。

父 POM (修改后):

1
2
3
4
5
6
7
8
<groupId>com.pointer.pay</groupId>
<artifactId>pointer-pay</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>

<properties>
<revision>1.0.0-SNAPSHOT</revision>
</properties>

接着,修改所有子模块的 <parent> 片段,使其版本号也指向 ${revision}

子模块 POM (修改后):

1
2
3
4
5
6
<parent>
<groupId>com.pointer.pay</groupId>
<artifactId>pointer-pay</artifactId>
<version>${revision}</version>
</parent>
<artifactId>pointer-pay-adapter</artifactId>

4.3. 第二步:集成 flatten-maven-plugin

在父 POM 的 <build> -> <plugins> 区域中,添加 flatten-maven-plugin 的完整配置,如 3.1 章节所示。

4.4. 最终效果验证

完成以上改造后,执行 mvn clean install 命令。构建成功后,可以观察到以下变化:

  1. 生成 .flattened-pom.xml:在每个模块的根目录下,都会生成一个名为 .flattened-pom.xml 的新文件。
  2. 版本号正确解析:打开任意一个 .flattened-pom.xml 文件,会发现 <version> 标签中的 ${revision} 已经被成功替换为 <properties> 中定义的实际版本号,例如 <version>1.0.0-SNAPSHOT</version>
  3. 仓库构件正确:检查本地 Maven 仓库(或远程私服),可以看到新安装(或部署)的构件 pom 文件内容与 .flattened-pom.xml 一致,版本号已正确解析。

通过这一系列操作,我们成功地实现了多模块项目的版本统一管理,不仅提升了日常开发的效率,也保证了构建和部署过程的健壮性。