阅读视图

发现新文章,点击刷新页面。

Maven父子模块Deploy的那些坑

起因

前两天遇到个挺坑的问题。我们有个基础服务框架叫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的时候就报错了:

Could not find artifact com.financial:message-client:jar:1.3.0-SNAPSHOT

我当时就懵了,明明刚deploy上去啊,怎么就找不到呢? 去Nexus私服上看,message-client-1.3.0-SNAPSHOT.jar确实在那儿躺着,但就是拉不下来。

后来发现Maven在尝试下载依赖的时候会报pom找不到的警告:

Could not find artifact com.financial:financial-platform:pom:1.3.0-SNAPSHOT

恍然大悟

这时候才反应过来,虽然message-client不依赖common-utils或db-starter这些兄弟模块,但是它的pom.xml里有这么一段:

<parent>
    <groupId>com.financial</groupId>
    <artifactId>financial-platform</artifactId>
    <version>1.3.0-SNAPSHOT</version>
</parent>

Maven拉取message-client的时候,会先去找它的父pom。父pom找不到,后面的事儿就都黄了。

整个依赖解析的流程是这样的:

sequenceDiagram
    participant B as 业务系统
    participant N as Nexus私服
    participant P as financial-platform
    participant M as message-client
    
    B->>N: 请求message-client:1.3.0-SNAPSHOT
    N->>N: 找到message-client的jar
    N->>N: 读取message-client的pom
    N->>P: 需要financial-platform:1.3.0-SNAPSHOT的pom
    P-->>N: 404 Not Found
    N-->>B: 依赖解析失败

为什么需要父pom

有人可能会问,message-client都已经是个完整的jar了,为什么还要父pom呢?

其实父pom里会定义很多东西:

<!-- 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上去:

# 在financial-platform父工程目录执行
mvn clean deploy

这样Maven会把父pom和所有子模块都发布到Nexus。即使你只改了message-client,父pom也得发上去,因为版本号变了。

Maven的继承和聚合

说到这儿,顺便聊聊Maven的继承和聚合,很多人容易搞混。

继承是子模块继承父pom的配置,通过<parent>标签实现。聚合是父工程管理多个子模块,通过<modules>标签实现。

graph TB
    subgraph 继承关系
    P1[financial-platform<br/>配置和依赖版本] -.继承.-> C1[common-utils<br/>使用父配置]
    P1 -.继承.-> C2[message-client<br/>使用父配置]
    P1 -.继承.-> C3[db-starter<br/>使用父配置]
    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里是这样的:

<!-- 聚合: 管理有哪些子模块 -->
<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里是这样的:

<!-- 继承: 指定从哪个父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单个模块。

#!/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:

<!-- 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上去,省得业务系统那边找你麻烦。

❌