Maven 多模块 Spring Boot repackage 让被依赖模块变成 fat jar 的编译失败问题

Maven 多模块 Spring Boot repackage 让被依赖模块变成 fat jar 的编译失败问题

记录时间:2026-09-02
环境:Jenkins 2.568.1(K8s 动态 Slave,共享库 jarHelmDevopsMultByk8s 流水线)/ maven 容器 gzeport-maven:3.8.6-openjdk-11 / 项目 sccg-jar-gzeport-market-services(Spring Boot 2.5.14 多模块,spring-boot-maven-plugin 2.1.1.RELEASE)

一、问题现象

sccg 市场采购的 jar 项目 gzeport-market-services 做服务扩容:原来只部署 gzeport-market-service 一个服务,现在要加 gzeport-market-httpent-servicegzeport-market-httpbank-service 两个新服务。Jenkinsfile 的 GET_PROJECT_NAME 加了逗号分隔的三个服务后,httpbank 连续三轮构建失败(#34/#35/#38),全是同一个错:

[ERROR] package com.gzeport.market.server.service does not exist
[ERROR] /.../PosForeServiceImpl.java:[64,13] cannot find symbol
  symbol:   class MerchantShRegisterService

报错的全是 httpbank 的 service/impl 实现类,缺的接口(MerchantShRegisterServiceForeignBankQueryServiceSettlementQueryService 这些)都在 com.gzeport.market.server.service 包下。httpent 没报错。奇怪的是开发本地 IDE 编译完全正常,一到 CI 就挂。

二、排查过程

2.1 前两轮判断(都错了)

第一轮怀疑版本错位:httpbank 对 gzeport-market-service 的依赖写死了 1.6.01-RELEASE,其他兄弟模块都是 ${gzeport.market.version},我判断 CI 会因此去私服拉旧 jar。业务改成属性引用后重跑(#35),报错不变——翻顶层 pom 才发现属性值本来就是 1.6.01-RELEASE,改前改后是同一个版本,判断依据不成立。

第二轮怀疑 com.gzeport.market.server.service 包在拆分后被挪进了别的模块(workspace 里有 gzeport-market-core 等顶层目录)。grep 业务源码定位,接口和实现全在 httpbank 已依赖的 gzeport-market-service 里,这条也排除了。

教训:看到「写死的版本号与属性值不同」要先核对属性实际值再下结论,我跳过了这步。

2.2 定位真凶:repackage 原地替换

包在依赖模块里、版本也对、reactor 也把 market-service 构建进去了,那问题只剩一个可能:httpbank 编译时拿到的 market-service jar 和源码构建出来的不是同一个东西。

我打开 gzeport-market-service 的 pom 看 build 段,找到了:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <version>2.1.1.RELEASE</version>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <includeSystemScope>true</includeSystemScope>
        <mainClass>com.gzeport.market.server.MarketServerApplication</mainClass>
    </configuration>
</plugin>

repackage 没配 <classifier>。这是 spring-boot 插件的默认行为:package 阶段把 target/gzeport-market-service.jar 原地替换成可执行 fat jar,自身类文件全部挪进 BOOT-INF/classes/,依赖塞进 BOOT-INF/lib/。jar 根目录下不再有 com/ 目录。

整条链对上了:

  • -am 把 market-service 排进 reactor 并构建成功(它是启动模块,fat jar 正是它要的产物)
  • httpbank 编译时从 reactor 拿到这个 fat jar,jar 根下没有 com/gzeport/market/server/service/,报 package does not exist
  • 全仓库 grep 下来依赖 market-service 的只有 httpbank 一家,所以只有 httpbank 挂
  • 本地 IDE 能编过是因为 IDE 直接拿工作区源码解析依赖,不走 Maven 构件,把问题盖住了

对照其他模块,这个解释也自洽:common-servicefile-storage-service 是纯库(没有 spring-boot 插件),被谁依赖都没事;httpent/httpbank 自己也配了 repackage,但没有下游消费者,fat jar 正是部署想要的形态。坑只留给被人依赖还配 repackage 的模块。

三、根因分析

gzeport-market-service 是「双身份」模块:既是 httpbank 的依赖库,又是独立部署的启动服务。两种身份对 jar 形态的要求互斥,repackage 没配 classifier 时只能二选一,选了部署形态就伤了依赖方。

修复思路就是让两种形态并存:给 repackage 配 <classifier>exec</classifier>,fat jar 另存为 -exec.jar,原 jar 保持纯库形态留在原位,依赖方和部署方各取所需。

四、解决方案

4.1 业务侧(根因修复)

gzeport-market-server/gzeport-market-service/pom.xml 的 spring-boot 插件配置加一行:

<configuration>
    <classifier>exec</classifier>
    <includeSystemScope>true</includeSystemScope>
    <mainClass>com.gzeport.market.server.MarketServerApplication</mainClass>
</configuration>

注意:这个插件配置只能加在 market-service 自己的 pom 里。顶层聚合 pom 的 <build><plugins> 是全局继承的(顶层那块还挂着 flatten 插件),加在那里会让 dao、core 这些纯库模块也执行 repackage,问题直接扩大到所有模块。

注意<includeSystemScope>true</includeSystemScope> 别顺手去掉,market-service 依赖了 system scope 的 lib/iTextAsian.jar(PDF 中文字体),去掉后字体丢失。

4.2 流水线侧(连带适配)

CI 镜像阶段的产物准备原来是 mv *.jar app.jar,target 下只能有一个 jar。加 classifier 后 market-service 的 target 会有两个 jar,多源 mv 会报错。我改了共享库 vars/stageBuildAndPushImage.groovy 的产物检查逻辑:存在 *-exec.jar 时先删掉薄库 jar 再把 exec jar 改名为 app.jar;没有 exec jar 走原逻辑。

注意:第一版我写的是 mv *-exec.jar app.jar 后跟 rm -f ./*.jar,推演时发现 app.jar 自己也匹配 *.jar,会把它一起删掉。改成 find . -maxdepth 1 -name "*.jar" ! -name "*-exec.jar" -delete 用排除法先清理,再 mv。这类通配符加删除的组合要过一遍展开结果再用。

我还去 plugin.xml 里确认过 spring-boot-maven-plugin 2.1.1 的 classifier 参数不支持 -D 命令行传参(整个插件只有 skip 支持),所以「流水线不动、命令行加参数」这条路不存在,pom 必须改。

五、验证

业务推送 pom 修改后重跑 #40,SUCCESS,3 分 1 秒。镜像阶段日志把两种分支都验证了:

market-service 的 target 出现双 jar,走新的 exec 分支:

-rw-r--r-- 1 root root 178005577 gzeport-market-service-exec.jar
-rw-r--r-- 1 root root   2102276 gzeport-market-service.jar
+ ls gzeport-market-service-exec.jar
+ find . -maxdepth 1 -name '*.jar' '!' -name '*-exec.jar' -delete
+ mv gzeport-market-service-exec.jar app.jar

httpbank/httpent 没有 exec 命名,走原通用分支(ls '*-exec.jar' 无命中后 mv xxx.jar app.jar),行为与之前完全一致,零回归。三个镜像全部推送成功:gzeport-market-service / httpent-service / httpbank-service : dev-20260902-40

httpbank 编译通过本身就说明纯库形态的 jar 回到了 reactor classpath,整条根因链都对上了。

六、注意事项

  • httpent 为什么没事:它的 pom 根本没引 market-service,依赖链上没有任何被 repackage 污染的构件。判断这类问题先画依赖图,谁消费了「被打成 fat jar 的模块」,谁就会炸。
  • 这个结构本身就是设计问题:market-service「既被人依赖、又自己部署」是微服务拆分拆了一半的形态。加 classifier 是止血,正解是把接口抽成独立 api 模块(如 gzeport-market-api),httpbank 依赖接口走远程调用,而不是进程内 import 实现。还有个运行期隐患:httpbank 的 fat jar 里现在塞着整个 market-service(178MB 里有它一份),改了 market-service 代码后 httpbank 不重新构建就还在跑旧逻辑。
  • 类似项目的提前识别:多模块 jar 项目接入 CI 时,如果某个服务既出现在部署清单里、又出现在其他服务的 pom 依赖里,提前让业务加 classifier,别等编译失败再排查。

七、参考资料

  • spring-boot-maven-plugin repackage 参数说明(classifier 语义:不配则原地替换主构件,配了则附加为补充构件):https://docs.spring.io/spring-boot/docs/2.1.1.RELEASE/maven-plugin/repackage-mojo.html
  • 验证命令:mvn -pl <模块> -am clean package 后看 target 下 jar 的目录结构(unzip -l xxx.jar | grep BOOT-INF 判断 fat jar)
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇