Maven多模块工程最佳实践:公共组件的版本管理与私服发布
Maven多模块工程最佳实践:公共组件的版本管理与私服发布
适读人群:负责公共组件或平台基础设施的Java开发者 | 阅读时长:约16分钟
开篇故事
两年前我接手一个老项目,十几个微服务,每个服务都有自己的 pom.xml,里面 Spring Boot 的版本有 2.5.6、2.6.1、2.7.0 三个,Jackson 的版本有四种,公司内部工具包的版本更是乱得一塌糊涂,1.0.0、1.0.1、1.1.0-SNAPSHOT 夹杂其中。
有一次排查一个序列化问题,花了整整半天,最后发现是两个服务用了不同版本的 Jackson,序列化行为有差异。我当时的心情只能用"心如死灰"来形容。
那之后我花了两周时间,把整个项目改造成 Maven 多模块工程,建了统一的 BOM(Bill of Materials),规范了私服发布流程。从那以后,"依赖版本冲突"这个词在我们组的故障复盘里基本消失了。
今天把这套实践完整分享出来。
一、多模块工程的组织方式
1.1 典型目录结构
一个中等规模的微服务平台,多模块工程的层次应该这样设计:
company-platform/
├── pom.xml # 顶层父POM(只做版本管理)
├── platform-bom/ # BOM模块(依赖声明仓库)
│ └── pom.xml
├── platform-parent/ # 公共父POM(插件、编译配置)
│ └── pom.xml
├── platform-common/ # 公共工具类
│ └── pom.xml
├── platform-starters/ # 自定义Starter集合
│ ├── pom.xml
│ ├── sms-spring-boot-starter/
│ ├── oss-spring-boot-starter/
│ └── distributed-lock-starter/
└── services/ # 业务服务(各自独立Git仓库)
├── user-service/
└── order-service/1.2 三层POM的职责划分
二、核心文件深度解析
2.1 顶层父POM
<?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.company</groupId>
<artifactId>company-platform</artifactId>
<version>2.1.0</version>
<packaging>pom</packaging>
<name>Company Platform Parent</name>
<!-- 顶层POM只管理子模块,不引入任何依赖 -->
<modules>
<module>platform-bom</module>
<module>platform-parent</module>
<module>platform-common</module>
<module>platform-starters</module>
</modules>
<!-- 集中管理所有子模块和三方库的版本号 -->
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Spring生态 -->
<spring-boot.version>3.2.0</spring-boot.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
<!-- 数据库 -->
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<druid.version>1.2.21</druid.version>
<!-- 工具库 -->
<hutool.version>5.8.25</hutool.version>
<guava.version>33.0.0-jre</guava.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<!-- 平台内部组件版本 -->
<platform.version>2.1.0</platform.version>
</properties>
</project>2.2 BOM 模块:版本的"唯一真相"
BOM(Bill of Materials)是 Maven 多模块工程的核心,它的唯一职责就是声明版本,不引入任何实际依赖。
<?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>
<parent>
<groupId>com.company</groupId>
<artifactId>company-platform</artifactId>
<version>2.1.0</version>
</parent>
<artifactId>platform-bom</artifactId>
<!-- 关键:packaging必须是pom -->
<packaging>pom</packaging>
<name>Platform BOM</name>
<dependencyManagement>
<dependencies>
<!-- 导入Spring Boot的BOM,继承它的版本管理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 导入Spring Cloud的BOM -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 导入Spring Cloud Alibaba的BOM -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 声明平台内部组件版本 -->
<dependency>
<groupId>com.company</groupId>
<artifactId>platform-common</artifactId>
<version>${platform.version}</version>
</dependency>
<dependency>
<groupId>com.company</groupId>
<artifactId>sms-spring-boot-starter</artifactId>
<version>${platform.version}</version>
</dependency>
<dependency>
<groupId>com.company</groupId>
<artifactId>oss-spring-boot-starter</artifactId>
<version>${platform.version}</version>
</dependency>
<!-- 三方库版本统一管理 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>2.3 platform-parent:插件和编译规范
<?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>
<parent>
<groupId>com.company</groupId>
<artifactId>company-platform</artifactId>
<version>2.1.0</version>
</parent>
<artifactId>platform-parent</artifactId>
<packaging>pom</packaging>
<!-- 导入BOM,让子模块可以不写版本号 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.company</groupId>
<artifactId>platform-bom</artifactId>
<version>${platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<!-- 统一Java编译版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<!-- 支持Lombok + MapStruct同时使用 -->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- 统一source/javadoc插件(发布到私服时需要) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 跳过测试(CI流水线会单独跑测试) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<!-- 发布到私服的配置 -->
<distributionManagement>
<repository>
<id>nexus-releases</id>
<name>Nexus Release Repository</name>
<url>http://nexus.company.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>nexus-snapshots</id>
<name>Nexus Snapshot Repository</name>
<url>http://nexus.company.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
</project>2.4 业务服务如何接入
业务服务只需要继承 platform-parent,所有版本自动继承:
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<!-- 继承公司内部parent,而不是spring-boot-starter-parent -->
<parent>
<groupId>com.company</groupId>
<artifactId>platform-parent</artifactId>
<version>2.1.0</version>
</parent>
<groupId>com.company.user</groupId>
<artifactId>user-service</artifactId>
<version>1.0.0</version>
<dependencies>
<!-- 不需要写版本号,从BOM继承 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.company</groupId>
<artifactId>sms-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
</dependencies>
</project>三、版本发布流程设计
3.1 版本号规范
遵循语义化版本(Semantic Versioning):
MAJOR.MINOR.PATCH[-SNAPSHOT]
例:
2.1.0-SNAPSHOT → 开发中版本,可随时覆盖发布
2.1.0 → 正式版本,发布后不可修改
2.1.1 → 只有BugFix的补丁版本
2.2.0 → 新增功能但向后兼容
3.0.0 → 有不兼容的API变更3.2 发布流程
3.3 maven-release-plugin 自动化发布
<!-- 在 platform-parent 的 build/pluginManagement 里添加 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<!-- 发布时跳过测试(测试在CI阶段已经跑过了) -->
<arguments>-DskipTests</arguments>
<!-- 标签名格式 -->
<tagNameFormat>v@{project.version}</tagNameFormat>
<!-- 自动push到Git -->
<pushChanges>true</pushChanges>
<!-- 使用批处理模式,不交互 -->
<preparationGoals>clean verify</preparationGoals>
</configuration>
</plugin>发布命令:
# 准备发布:检查工作区是否干净、修改版本号、跑测试
mvn release:prepare
# 执行发布:部署到私服,打Tag
mvn release:perform
# 如果出错可以回滚
mvn release:rollback四、踩坑实录
坑1:BOM 的 import 顺序影响版本优先级
症状:dependencyManagement 里同时 import 了 spring-boot-dependencies 和 spring-cloud-dependencies,某个依赖的版本不对。
根因:多个 BOM 都声明了同一个依赖的版本时,先声明的优先(不同于 dependencies 里的依赖冲突解决规则)。
正确做法:把更"细粒度"的 BOM 放在前面。Spring Cloud 里有些依赖版本会覆盖 Spring Boot 的,如果你希望 Spring Cloud 的优先,把它放前面。
<dependencyManagement>
<dependencies>
<!-- 优先级:先声明的优先 -->
<!-- 1. 我们自己的BOM优先级最高 -->
<dependency>
<groupId>com.company</groupId>
<artifactId>platform-bom</artifactId>
<version>${platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 2. Spring Cloud(会覆盖Boot里的部分版本) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 3. Spring Boot基础BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>坑2:子模块版本没有同步更新导致构建失败
症状:手动修改顶层 pom.xml 的版本号,忘了改子模块的 <parent> 版本,导致 mvn install 时找不到 parent。
解决方案:使用 versions-maven-plugin 统一修改所有模块的版本:
# 将整个项目版本设为 2.2.0-SNAPSHOT
mvn versions:set -DnewVersion=2.2.0-SNAPSHOT
# 确认修改
mvn versions:commit
# 如果不满意,回滚
mvn versions:revert坑3:Nexus 上 Release 版本被意外覆盖
症状:同一个 Release 版本(如 2.1.0)发布了两次,第二次把第一次的 JAR 覆盖了,导致其他服务行为异常。
根因:Nexus 的 Release 仓库配置了 Allow Redeploy,应该设为 Disable Redeploy。
在 Nexus 管理界面:Repositories → maven-releases → Hosted → Deployment policy → Disable redeploy。
代码层面的防护:在 CI 脚本里加检查:
#!/bin/bash
VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
if [[ $VERSION != *"SNAPSHOT"* ]]; then
# 检查该版本是否已经在私服存在
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
"http://nexus.company.com/repository/maven-releases/com/company/platform-bom/${VERSION}/platform-bom-${VERSION}.pom")
if [ "$HTTP_CODE" = "200" ]; then
echo "ERROR: Release版本 $VERSION 已存在,禁止覆盖发布!"
exit 1
fi
fi坑4:SNAPSHOT 版本在生产环境被使用
症状:某个服务部署到生产后行为异常,排查发现用了 1.2.0-SNAPSHOT 版本的公共包,而不是 1.2.0。
根因:CI 流水线没有禁止生产环境使用 SNAPSHOT。
解决方案:在 maven-enforcer-plugin 里添加规则:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>enforce-no-snapshots-in-release</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<!-- 如果当前是release版本,禁止依赖SNAPSHOT -->
<requireReleaseDeps>
<message>Release版本不允许依赖SNAPSHOT!</message>
<onlyWhenRelease>true</onlyWhenRelease>
<failWhenParentIsSnapshot>false</failWhenParentIsSnapshot>
</requireReleaseDeps>
</rules>
<fail>true</fail>
</configuration>
</execution>
</executions>
</plugin>坑5:多模块工程 mvn install 顺序问题
症状:在 CI 里直接 mvn install -pl user-service,报找不到 platform-common。
根因:-pl 指定子模块时,Maven 不会自动构建它依赖的其他子模块。
正确用法:
# 构建指定模块及其所有依赖
mvn install -pl user-service -am
# -am = --also-make:同时构建依赖的模块
# -amd = --also-make-dependents:同时构建依赖此模块的模块(发布场景用)五、总结与延伸
Maven 多模块的核心价值是版本的单一真相:所有依赖版本只在一个地方声明,所有服务从同一个地方继承。
几个关键原则总结:
- BOM 只管版本,不引依赖:
dependencyManagementvsdependencies的区别一定要清楚。 - Release 版本不可覆盖:Nexus 级别的限制 + CI 级别的检查双重保障。
- 版本工具替代手动修改:
versions-maven-plugin和maven-release-plugin能避免大量人为失误。 - enforcer-plugin 是护城河:在构建阶段就拦截问题,比运行时发现要早得多。
下一篇(455)进入 Java SPI 机制的深度解析,会对比今天讲的 spring.factories 机制,看看两者的本质异同。
