起因
前两天遇到个挺坑的问题。我们有个基础服务框架叫financial-platform,是典型的父子结构,父工程下面挂了common-utils、message-client、db-starter这几个子模块。这次需要升级message-client模块,增加了RocketMQ的一些新特性,版本从1.2.5-SNAPSHOT改到1.3.0-SNAPSHOT。
当时想的挺简单的,就是把整个项目的版本都改了,然后只deploy这个message-client模块上去就行了。毕竟这个模块看起来挺独立的,也不依赖其它兄弟模块,应该没问题吧?
结果被现实教育了。
拉取失败
改完版本号,deploy上去后,业务系统引用这个message-client的时候就报错了:
arduino
Could not find artifact com.financial:message-client:jar:1.3.0-SNAPSHOT
我当时就懵了,明明刚deploy上去啊,怎么就找不到呢? 去Nexus私服上看,message-client-1.3.0-SNAPSHOT.jar确实在那儿躺着,但就是拉不下来。
后来发现Maven在尝试下载依赖的时候会报pom找不到的警告:
arduino
Could not find artifact com.financial:financial-platform:pom:1.3.0-SNAPSHOT
恍然大悟
这时候才反应过来,虽然message-client不依赖common-utils或db-starter这些兄弟模块,但是它的pom.xml里有这么一段:
xml
<parent>
<groupId>com.financial</groupId>
<artifactId>financial-platform</artifactId>
<version>1.3.0-SNAPSHOT</version>
</parent>
Maven拉取message-client的时候,会先去找它的父pom。父pom找不到,后面的事儿就都黄了。
整个依赖解析的流程是这样的:
为什么需要父pom
有人可能会问,message-client都已经是个完整的jar了,为什么还要父pom呢?
其实父pom里会定义很多东西:
xml
<!-- financial-platform父pom里通常有这些 -->
<properties>
<java.version>11</java.version>
<spring-boot.version>2.7.18</spring-boot.version>
<rocketmq.version>4.9.7</rocketmq.version>
...
</properties>
<dependencyManagement>
<dependencies>
<!-- 统一管理RocketMQ、Redis、PostgreSQL等版本 -->
...
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<!-- 插件配置 -->
...
</pluginManagement>
</build>
message-client的pom可能会引用父pom里定义的属性和配置。Maven需要把父子pom合并起来,才能得到一个完整的、可执行的pom。
Maven构建有效pom的过程很简单:解析子模块pom时,如果发现有parent标签,就去Nexus找父pom。找到后合并父子配置,如果父pom还有parent,就继续往上找。一直找到最顶层,然后从上到下合并所有配置,最后生成一个完整的有效pom。
正确的做法
所以正确的做法是,把父pom和message-client都deploy上去:
bash
# 在financial-platform父工程目录执行
mvn clean deploy
这样Maven会把父pom和所有子模块都发布到Nexus。即使你只改了message-client,父pom也得发上去,因为版本号变了。
Maven的继承和聚合
说到这儿,顺便聊聊Maven的继承和聚合,很多人容易搞混。
继承是子模块继承父pom的配置,通过<parent>标签实现。聚合是父工程管理多个子模块,通过<modules>标签实现。
配置和依赖版本] -.继承.-> C1[common-utils
使用父配置] P1 -.继承.-> C2[message-client
使用父配置] P1 -.继承.-> C3[db-starter
使用父配置] end subgraph 聚合关系 P2[financial-platform] --聚合--> C4[common-utils] P2 --聚合--> C5[message-client] P2 --聚合--> C6[db-starter] end style P1 fill:#e1f5ff style P2 fill:#ffe1f5
父pom里是这样的:
xml
<!-- 聚合: 管理有哪些子模块 -->
<modules>
<module>common-utils</module>
<module>message-client</module>
<module>db-starter</module>
</modules>
<!-- 继承: 提供给子模块的配置 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>${rocketmq.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
子模块message-client里是这样的:
xml
<!-- 继承: 指定从哪个父pom继承 -->
<parent>
<groupId>com.financial</groupId>
<artifactId>financial-platform</artifactId>
<version>1.3.0-SNAPSHOT</version>
</parent>
<!-- 实际使用的依赖,版本从父pom继承 -->
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<!-- 版本号从父pom的dependencyManagement继承 -->
</dependency>
</dependencies>
这两个是独立的机制,可以单独使用。但大部分时候我们会一起用,既让父工程聚合管理子模块,又让子模块继承父配置。
后来我们的处理
我们现在的做法是,每次版本升级,不管改了几个模块,都执行完整的deploy。虽然会把common-utils、message-client、db-starter都发一遍,有点浪费,但起码不会出幺蛾子。
另外在Jenkins的CI流程里加了个检查,如果pom的版本号变了,必须全量deploy,不允许只deploy单个模块。
bash
#!/bin/bash
# Jenkins里的检查脚本
VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
if [[ $VERSION == *"SNAPSHOT"* ]]; then
echo "检测到SNAPSHOT版本: $VERSION"
echo "执行全量deploy到Nexus"
mvn clean deploy -DskipTests
else
echo "Release版本: $VERSION"
# release版本走发布审批流程
echo "需要审批后才能deploy"
exit 1
fi
实际案例分析
我们再看一个实际的场景。假设业务系统order-service需要引用我们升级后的message-client:
xml
<!-- order-service的pom.xml -->
<dependencies>
<dependency>
<groupId>com.financial</groupId>
<artifactId>message-client</artifactId>
<version>1.3.0-SNAPSHOT</version>
</dependency>
</dependencies>
Maven构建order-service的时候,会先从本地或Nexus下载message-client的jar和pom。读取message-client的pom时发现它依赖父pom financial-platform:1.3.0,于是继续去找父pom。如果父pom不存在,整个构建就失败了。找到父pom后,Maven会合并父子配置,然后递归解析所有传递依赖,最后才能成功构建。
所以你看,这是个链式反应。中间任何一环缺失,整个构建都会挂掉。
就这样吧,希望能帮到遇到类似问题的朋友。这个坑我们已经踩过了,你们就别再踩了。下次升级message-client加新功能的时候,记得把整个framework都deploy上去,省得业务系统那边找你麻烦。