父 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 compiletestpackage
触发 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-pluginrepackage

判断标准很简单:

模块类型 有 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>

五、复盘

这个坑为什么特别值得记

它集齐了三个特征:

  1. 隐式继承repackage 的绑定你没写,是父 pom 给的。看自己的 pom 找不出问题。
  2. 环境差异:IDEA 正常,命令行才炸。开发时完全无感。
  3. 报错位置反直觉:报的是 api-cloud-common,但你想构建的是 user-service——是被 -am 牵连的。

一条通用经验

父 pom 给你的东西,比你自己写的东西更危险。

自己写的配置出问题,你能顺着自己的代码找到;父 pom 隐式绑定的东西出问题,你的第一反应是"这不可能",然后花时间在错误的方向上。

对应的做法:在多模块项目里,明确列出每个模块的"类型"——是应用还是库。库模块主动跳过应用相关的插件(不只是 repackage,还包括 spring-boot:rundocker 插件等)。

本项目后来在 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/

  • 0