父 pom 的 spring-boot-maven-plugin 正在污染你的纯库模块
只在服务器构建时失败:Unable to find main class,IDEA 里却一切正常。根因是父 pom 把 repackage 目标继承给了纯库模块。
一、症状:一个"不可能出错"的地方报错了
在服务器上构建镜像时,Maven reactor 直接失败:
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:3.4.5:repackage
(repackage) on project api-cloud-common:
Execution repackage of goal org.springframework.boot:spring-boot-maven-plugin:3.4.5:repackage failed:
Unable to find main class
[ERROR]
[ERROR] After correcting the problems, please re-run the build or use `-DskipTests` ...
api-cloud-common 是一个纯工具库,本来就不该有 main 方法。 为什么它会去执行 repackage?
更让人困惑的是:在 IDEA 里构建完全正常。
二、根因:隐式继承 + IDEA 与命令行的行为差异
第一步:repackage 是怎么被绑上的
父 pom 继承自 Spring Boot 的 parent:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.5</version>
</parent>
spring-boot-starter-parent 在它的 pluginManagement 里,为 spring-boot-maven-plugin 预绑定了 repackage 目标:
<!-- spring-boot-starter-parent 内部大致是这个意思 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal> <!-- 绑定到 package 阶段 -->
</goals>
</execution>
</executions>
</plugin>
pluginManagement 本身只是"管理",但 executions 的绑定会被子模块继承。于是:
spring-boot-starter-parent
→ 所有子模块的 package 阶段都挂上了 repackage
repackage 的职责是"把普通 jar 重新打包成可执行 fat jar",它必须能找到 main 方法。而 api-cloud-common 是纯工具库,没有 main:
Unable to find main class
第二步:为什么 IDEA 不报错
因为 IDEA 默认只跑到 compile 阶段。
| IDEA 构建 | 命令行 mvn package / CI / Docker 构建 |
|
|---|---|---|
| 执行阶段 | compile |
compile → test → package |
触发 repackage |
❌ 不会 | ✅ 会 |
| 结果 | 一切正常 | Unable to find main class |
这是一个典型的"本地开发环境掩盖生产构建问题"的坑。 它的危险在于:你已经习惯"IDEA 编译通过 = 没问题",而这个问题在 IDEA 里永远暴露不出来。
第三步:为什么偏偏是 Common 先炸
Docker 构建命令里带了 -am:
mvn -B clean package -DskipTests -pl api-cloud-service-user -am
-am(also make)表示"同时构建该模块依赖的所有模块"。api-cloud-service-user 依赖 api-cloud-common,所以构建顺序是:
api-cloud-common → api-cloud-service-user
↑
在这里就炸了,后面的根本不会执行
整个 reactor 因此失败。
三、修复:显式跳过 repackage
在 api-cloud-common/pom.xml 里显式覆盖配置:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
注意:这里只是覆盖 configuration,不需要重写 executions。 因为父 pom 已经把 repackage 绑定好了,skip=true 会让它跳过执行。
如果想彻底一点,也可以把
repackage的 execution 直接去掉:<executions> <execution> <id>repackage</id> <phase>none</phase> <!-- 解除绑定 --> </execution> </executions>但
<skip>true</skip>更简单、更明确地表达了意图:"这个模块不是可执行应用"。
四、判据
在 Maven 多模块项目里,任何"没有 main 方法"的模块(common / sdk / api / model),都必须显式跳过
spring-boot-maven-plugin的repackage。
判断标准很简单:
| 模块类型 | 有 main? | 需要 repackage? |
需要 <skip>true</skip>? |
|---|---|---|---|
| 可执行应用(user / business / order / gateway) | ✅ | ✅ 打成 fat jar | ❌ |
| 纯库(common) | ❌ | ❌ | ✅ |
一句话检查法
# 在整个项目里找所有没有 main 方法但继承 spring-boot-starter-parent 的模块
grep -rL "public static void main" --include="*.java" */src/main/java/ | cut -d/ -f1 | sort -u
这些模块的 pom 里都应该有 <skip>true</skip>。
五、复盘
这个坑为什么特别值得记
它集齐了三个特征:
- 隐式继承:
repackage的绑定你没写,是父 pom 给的。看自己的 pom 找不出问题。 - 环境差异:IDEA 正常,命令行才炸。开发时完全无感。
- 报错位置反直觉:报的是
api-cloud-common,但你想构建的是user-service——是被-am牵连的。
一条通用经验
父 pom 给你的东西,比你自己写的东西更危险。
自己写的配置出问题,你能顺着自己的代码找到;父 pom 隐式绑定的东西出问题,你的第一反应是"这不可能",然后花时间在错误的方向上。
对应的做法:在多模块项目里,明确列出每个模块的"类型"——是应用还是库。库模块主动跳过应用相关的插件(不只是 repackage,还包括 spring-boot:run、docker 插件等)。
本项目后来在 CI/CD 的实际反馈里印证了这一点:这类问题只在命令行/CI/Docker 构建时暴露,本地 IDEA 全绿。
验证方式
修完之后,用命令行验证,不要用 IDEA:
# 必须带 -am,复现真实构建路径
mvn -B clean package -DskipTests -pl api-cloud-service-user -am
# 期望:BUILD SUCCESS
# 且 api-cloud-common/target/ 下只有一个普通 jar,没有 .original 后缀的文件
repackage成功执行过的标志是留下xxx.jar.original。如果 common 的 target 里出现了它,说明跳过得不对。
原创文章,作者:ECHO陈文,如若转载,请注明出处:https://www.luweipai.cn/ops/1789977241/