1. 打包失败的现场:先搞清楚这个报错在说什么
先说结论:repackage failed: Unable to find main class这个问题,十有八九不是你的代码逻辑写错了,而是 Spring Boot Maven 插件在 repackage 阶段找不到可执行入口。换句话说,你的项目能编译、能跑测试,但一到打包成可执行 jar 的时候就懵了——它不知道该从哪个类的main方法启动。
很多第一次踩这个坑的人会习惯性地去检查代码,以为是自己main方法写错了位置,或者类名拼写有问题。我最初也这样干过,翻来覆去找了很久,最后才发现问题根本不在业务代码里,而在项目结构和插件配置上。
理解这个报错之前,得先弄清楚 Maven 打包的整个链路。mvn package执行时,会经过 compile、test、jar 等多个阶段,最后如果你的项目引入了spring-boot-maven-plugin,它还会多执行一个repackage目标。这个目标的职责是把原本打出来的普通 jar 重新加工成一个"可执行的 fat jar"——也就是说,把项目自身的 class 文件、所有第三方依赖、以及 Spring Boot 的启动器全部打到一个 jar 包里,让你能用java -jar直接启动。
问题就出在这个 repackage 阶段:它需要找到一个带有main方法的类作为启动入口。如果找不到,就会直接报Unable to find main class,并且整个打包流程以失败告终。
有一点需要特别说明,这个报错和 JDK 版本没有必然关系,和 Maven 版本也没有必然关系。我见过有人升级 JDK、换 Maven 版本、甚至重装 IDEA,结果毫无变化。真正的关键在于:Spring Boot 插件在 repackage 时究竟去哪里找主类,以及你的项目结构是否满足它的查找条件。
为了方便理解,可以把 repackage 的过程想象成做一份打包好的外卖:编译好的代码是菜品,依赖是配菜和调料,而main方法就是"订单上写的送达地址"。没有地址,即便菜品再丰盛,外卖员也不知道往哪送。
2. 从根上拆解:为什么插件会找不到主类
2.1 父工程或模块结构带来的隐患
最常见的场景之一,就是多模块项目。假设你有一个父工程parent-project,下面有common-module、service-module、web-module三个子模块,那么问题可能出现在两个层面:
第一个层面,是父工程的 pom 里直接声明了spring-boot-maven-plugin,而且没有做任何精细化配置。一旦父工程自身也执行 package,插件就会尝试在父工程里找主类,但父工程通常只有 pom 文件没有源代码,自然找不到。
第二个层面更隐蔽:某些子模块(比如common-module)本身只是一个公共依赖模块,它既没有main方法,也不应该被打成可执行 jar。但如果子模块的 pom 继承了父工程里的插件配置,repackage 就会在common-module上执行,结果同样报错。
我自己遇到过的实际案例是:一个同事把公共工具类模块也配上了 Spring Boot 插件,每次打全量包的时候,构建任务在第一个模块就红了,日志里就是Unable to find main class。
这个问题的本质,是插件的作用范围没有控制好。合理做法是:只在真正需要打成可执行 jar 的模块上启用 repackage,其他模块要么不继承该插件,要么显式跳过执行。
2.2 打包配置里显式指定或排除
解决方案其实很直白:在需要打包成可执行 jar 的模块 pom 里,给插件配置mainClass。这样插件就不需要自己去"猜"了,直接按照你指定的类来找。
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.example.demo.DemoApplication</mainClass> </configuration> </plugin>有人会问:为什么 Spring Boot 插件不能自己识别主类呢?答案是可以识别的。如果你只有一个main方法,而且类的签名很标准(public static void main(String[] args)),插件通常能通过扫描 classpath 自动找到。但是如果你有多个类带有main方法,或者你的项目结构不够典型,插件就会陷入"选择困难",最终直接报错。
还有一种情况,是项目里确实有多个主类。比如你写了一个DemoApplication作为 Spring Boot 启动类,又写了一个TestMain作为临时测试入口。这种时候插件不知道谁是真正的启动入口,只能失败。
对于多模块里那些不需要打包成可执行 jar 的模块,建议使用skip参数把 repackage 跳过去:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <skip>true</skip> </configuration> </plugin>这样既保留了插件配置的统一性,又让每个模块该干什么干什么,不会互相干扰。
2.3 一个不常见但确实存在的坑:main 方法所在的类不在编译范围内
还有一种比较隐蔽的情况,是main方法存在的类确实写了,但pom.xml里通过<build>中的<resources>或<plugins>配置,把某些目录排除在了编译或打包范围之外。这种情况下,class 文件压根没有进入最终的 jar 包,插件自然找不到主类。
这种问题排查起来比较头疼,因为 IDE 里编译和运行是正常的,只有通过 Maven 打包才会暴露。遇到了别慌,先在命令行执行下面命令,确认主类是否真的被编译了:
mvn clean compile然后检查target/classes目录里有没有对应的.class文件。如果没有,说明问题出在编译范围配置上;如果有,再往下排查 jar 内容:
jar tf target/your-app.jar | grep DemoApplication通过这种逐步缩小范围的方式,远比盯着报错日志猜来猜去高效。
3. 实操复盘:我修复过的三种真实场景
3.1 单模块项目:最简单也最容易忽略
先看一个很基础的单模块项目。项目结构如下:
demo-app/ ├── pom.xml └── src/main/java/com/example/demo/ └── DemoApplication.javapom 里引入了spring-boot-starter-parent,也加了spring-boot-maven-plugin。按理说不会出问题,但有一个细节:DemoApplication类里的main方法可能是这样的:
public class DemoApplication { public void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }看到了吗?main方法漏写了static关键字。虽然 IDEA 里右键也能运行(它会自动补充一些信息),但 Spring Boot 插件在 repackage 阶段扫描时,要求必须是标准的public static void main(String[] args)签名。漏了static,插件就不会认为它是一个合法的启动入口。
这类问题排查最快的方法,就是打开 IDE 的事件日志或者看编译输出有没有警告。当然,直接检查main方法签名也是一眼就能看出来的事。
3.2 多模块项目:父 pom 插件配置的精准控制
再来看一个多模块项目的实际案例。我参与过的一个项目结构大致如下:
cloud-platform/ ├── pom.xml (parent) ├── common-utils/ ├── order-service/ └── user-service/父 pom 里做了这些事情:定义依赖管理、统一插件版本、声明spring-boot-maven-plugin。问题是,common-utils其实只是一个工具包,它不应该被 repackage。
当时的报错很典型:
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.7.8:repackage (repackage) on project common-utils: repackage failed: Unable to find main class原因很清楚:父 pom 里声明了插件,而common-utils作为子模块默认继承了这个插件。解决办法有两种。
第一种,是在父 pom 里把spring-boot-maven-plugin放到<pluginManagement>中,而不是直接放在<plugins>中。<pluginManagement>只做版本管理,不会强制子模块继承。然后,只在真正需要打成可执行 jar 的order-service和user-service模块里显式声明插件。
第二种,是在common-utils的 pom 里显式跳过:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <skip>true</skip> </configuration> </plugin>我个人倾向于第一种,因为从源头管住了插件的传播范围,子模块的职责更清晰。不过如果你的项目已经有很多模块,逐个改 pom 比较麻烦,第二种方案作为临时手段也完全可行。
3.3 微服务启动器与业务代码分离
还有一种模式,是启动类和业务代码分开放在不同模块或者不同目录里。最常见的就是你写了一个Application启动类,但又建了一个config包、controller包、service包,这些都没问题——只要所有代码都在同一个模块的编译范围内,插件就能找到启动类。
但有些项目为了保证启动加速、或者为了应对复杂的部署环境,会把启动类放在一个单独的bootstrap模块里,业务代码放在business模块中。这种情况下,bootstrap模块依赖了business模块,而需要执行 repackage 的是bootstrap模块。
只要你的bootstrap模块 pom 里配置了spring-boot-maven-plugin,并且mainClass指向该模块中的启动类,就不会有找不到主类的问题。关键点是:启动类必须在当前模块的 classpath 中。
如果启动类在business模块里,而你想在bootstrap模块里把它包成可执行 jar,那么bootstrap模块必须依赖business模块。这里需要留意的是,如果business模块也被声明了spring-boot-maven-plugin,且没有skip,它依然可能报错。所以,始终记住一条核心原则:只需要一个可执行 jar,那就只让一个模块做 repackage。
4. 一步步排查:不想再被报错折磨就看这里
4.1 先快速定位问题所在
我整理了一套排查顺序,直接照着走,大多数情况下能在五分钟内定位问题:
第一步,执行mvn clean package,记录完整报错信息,注意是哪个模块报的错。
第二步,检查该模块的 pom.xml,看是否引入了spring-boot-maven-plugin,以及有没有配置mainClass。
第三步,进入该模块源码目录,找到主类,确认main方法签名是标准的。
第四步,检查target/classes目录是否存在主类的.class文件。
第五步,如果以上都没问题,检查构建插件(如maven-compiler-plugin和maven-jar-plugin)是否有覆盖默认行为,比如把主类排除或改变了 jar 的 Main-Class 属性。
这套顺序看起来简单,但效率极高。大多数人的问题在第二步和第三步就能解决。
4.2 对多模块项目的针对性检查
多模块项目需要额外做一次"插件传播"检查。具体方法如下:
在父 pom 中查看spring-boot-maven-plugin是定义在<pluginManagement>里,还是直接定义在<plugins>里。
如果是后者,再逐一查看各子模块是否显式配置了<skip>true</skip>。
然后,针对每个子模块单独执行:
mvn spring-boot:repackage -DskipTests观察哪些模块会报错,哪些模块成功。这样就能快速圈定问题范围,不用整个项目一起构建。
如果你的项目模块特别多,还可以用 Maven 的-pl参数只构建指定模块:
mvn package -pl order-service -am-am的意思是同时构建依赖模块。这样执行速度快,排查也方便。
4.3 确认可执行 jar 是否真的可运行
即便报错排除了,打包成功后也建议做一次验证,避免交付出去的 jar 是个"半成品"。
java -jar target/order-service-1.0.0.jar如果启动顺利,会看到 Spring Boot 的启动日志。如果启动时报错找不到主类或其他依赖,可以用下面命令查看 jar 包内部结构:
jar tf target/order-service-1.0.0.jar重点检查是否有BOOT-INF/classes/目录和BOOT-INF/lib/目录。正常的 Spring Boot 可执行 jar 都包含这两个目录。如果没有,说明 repackage 没有真正执行成功——这可能是插件在 pom 中的声明位置不对,或者是执行顺序被覆盖了。
5. 避坑经验与实操心得
5.1 插件版本和父依赖版本要匹配
spring-boot-maven-plugin的版本通常会和spring-boot-starter-parent的版本保持一致。如果你手动指定了插件版本,却和 Spring Boot 版本不匹配,repackage 时的行为可能变得很诡异。比如某些较老的插件版本对 JDK 17 支持不佳,会有奇怪的扫描失败问题。
我的习惯是,除非有特殊需求,否则尽量使用 Spring Boot 父依赖统一管理的插件版本,不要手工指定:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent>这样插件版本自动对齐,省心。
5.2<mainClass>和<start-class>的关系
有读者可能见过另一种配置方式,是在 pom 的<properties>里设置<start-class>:
<properties> <start-class>com.example.demo.DemoApplication</start-class> </properties>这两种方式最终的作用是相似的,Spring Boot 插件会优先读取<mainClass>配置,其次是<start-class>,最后才是自动扫描。如果你两种都配置了,而且值不一致,请以<mainClass>的配置为准进行排查。
5.3 注意 IDE 的"伪编译"迷惑
IDEA 的 Build 和 Maven 的 compile 并不完全等价。IDEA 有自己的编译器,可能把你修改后的代码编译到target/classes,即便 pom 配置有问题也不会立刻暴露。所以无论你在 IDEA 里构建成功多少次,最终判断标准都应该以命令行执行mvn clean package为准。
如果你习惯用 IDEA 的 Maven 面板操作,建议在 Lifecycle 里先执行clean,再执行package,不要省略clean。不 clean 的话,旧的 class 文件可能残留,掩盖部分问题。
5.4 多个 main 方法并存时的处理
项目里偶尔会写一些工具类,里面带main方法临时测试。这种情况下,建议这类类名不要和*Application结尾的启动类混在同一个包下,或者干脆把临时测试类放到src/test/java目录下,让它们不出现在最终的 classpath 里。
如果无法避免多个main方法并存,就给插件配置mainClass,让它明确知道启动入口在哪。
6. 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 单模块项目报 Unable to find main class | main 方法签名非法 | 检查是否为public static void main(String[] args) |
| 多模块项目中报错模块没有主类 | 插件在纯工具模块中执行了 repackage | 在公共模块显式配置<skip>true</skip> |
| 报错模块没有配置任何插件 | 父 pom 插件声明被继承 | 将插件从<plugins>移到<pluginManagement> |
| 启动类存在但插件仍找不到 | 启动类不在当前模块的编译范围内 | 调整项目依赖关系,让启动类所在模块作为可执行模块 |
| 打包成功但运行时报错找不到主类 | repackage 未真正执行 | 检查是否有多个 spring-boot-maven-plugin 定义导致执行顺序异常 |
| 所有配置看起来都正常但仍报错 | 本地 Maven 仓库缓存了旧插件配置 | 执行mvn clean package -U强制更新快照 |
这张表是我在实际排查中最常用到的,很多问题本质上是同一个病因的不同表现。记住一个原则:遇到 repackage 报错,优先怀疑项目结构和插件配置,而不是业务代码,能帮你省下大量排查时间。
最后再分享一个我自己的小习惯:在项目的主 pom 里,我通常会为所有模块统一设置一个属性:
<properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>这不是直接解决Unable to find main class的方法,但能够保证所有模块编译参数一致,避免因为编译版本不统一导致的次生问题。毕竟很多看上去很离谱的报错,背后可能只是某些环境差异被放大了。