☰
Spring Boot Maven打包报错:Unable to find main class排查指南
2026/10/1 20:03:49 网站建设 项目流程

“repackage failed: Unable to find main class”,看到这行红字的时候,很多人的第一反应是——我的Application类明明写得好好的,凭什么说找不到?别急,这个报错我前前后后遇到过不下十次,每次的原因还都不太一样。尤其在用Maven打包Spring Boot项目时,这条报错出现的频率高得惊人,而且经常在项目本机IDEA里跑得好好的,一到命令行mvn clean package就翻车。

这个报错本身并不复杂,就发生在Spring Boot Maven插件执行repackage目标的时候:插件要重打一个可执行jar,但扫描完编译产物后,没找到任何可以作为启动入口的main方法。插件找不到“开机按钮”,自然拒绝继续往下走。文章后面我会把背后的原理、排查顺序、高频场景、以及一些常规文档里不会写的冷门坑全部整理出来,无论是第一次遇到这个报错的新手,还是被它反复折磨过的老手,都能在这里找到对应的解法。

1. 先弄明白repackage到底是什么,它为什么非要找一个main class

1.1 普通jar与Spring Boot可执行jar的差别

很多人对这个报错的第一反应是“我不就是打个包吗,Java代码能编译过不就行了”。问题就出在这里:Maven默认的package阶段产出的jar,和你平时用java -jar跑起来的Spring Boot可执行jar,完全是两种东西。

普通jar的行为很单纯,就是把target/classes下的class文件和资源文件原封不动地压缩成一个zip。这种jar能不能直接java -jar启动,取决于META-INF/MANIFEST.MF里有没有配置Main-Class,以及这个类在不在classpath上。但即便配置了Main-Class,普通jar也不会把你引用的那些第三方依赖jar一起打进去,所以直接跑还是会报ClassNotFoundException。

Spring Boot的可执行jar就不一样了。它要把当前项目的class、所有依赖jar、内嵌Tomcat/Jetty的运行环境、Spring Boot的启动加载器全部塞进一个“fat jar”里,让最终产物可以独立运行,放到哪台机器上都能直接java -jar。这个“重新打包”的动作,就是spring-boot-maven-plugin里repackage目标干的活。

可以这么理解:普通jar是一箱零件,可执行jar是一台组装好的整机。repackage就是最后那道“整机装配”工序,它必须知道哪个类是这台机器的“开机按钮”——也就是main方法入口。

1.2 repackage找主类的完整逻辑

那repackage具体怎么找这个main方法入口?它的查找逻辑是有优先级的,不是瞎扫一遍。大致是这样:

  • 如果spring-boot-maven-plugin配置里显式写了<mainClass>,直接用它。
  • 如果没写,接着看Maven属性里有没有<start-class>,有就用它。
  • 如果前面两个都没有,就去扫描编译输出目录target/classes下的所有class文件,从里面找一个带标准public static void main(String[] args)方法的类。

前两种属于“你告诉它主类在哪”,第三种属于“插件自己猜”。一旦这三种方式都落空,插件直接抛出一个MojoExecutionException,日志就是那句经典的:

[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.7.18:repackage (repackage) on project demo: repackage failed: Unable to find main class -> [Help 1]

这里有个细节很多人没注意:报错信息里的“main class”,和最终可执行jar的MANIFEST.MF里的Main-Class还不是同一个东西。Spring Boot可执行jar的Main-Class其实固定是org.springframework.boot.loader.JarLauncher,这是Spring Boot自己的启动加载器。真正你写的那个Application类,是被记录在Start-Class这个属性里的。所以Unable to find main class实际指的是“找不到可以填进Start-Class的应用主类”,不是指JarLauncher找不到。

1.3 为什么找不到主类就整个构建失败

有人会问:我就算不指定主类,先把jar打出来不行吗?不行。因为repackage目标一旦绑定到package阶段,它就要对生成的jar进行二次加工,加工的前提就是要能确定启动入口。

这个设定的逻辑其实很合理:repackage后的jar已经不是一个普通lib了,它是一个“可执行程序”。可执行程序连入口函数都没有,这道工序就失去意义了。所以Spring Boot团队在这里选择了“直接报错”而不是“打个警告然后放过去”。在CI流水线上,这种严格失败其实是好的,能把配置问题尽早暴露出来。

理解了这层逻辑,后面所有排查工作就有了方向:不是看Maven本身哪里坏了,而是看“为什么repackage在编译产物里找不到那个带main方法的类”。

2. 五层排查:按顺序检查,基本能定位九成问题

2.1 第一层:确认主类存在且代码正确

先说最基础的一层,也是最容易忽略的一层:你的主类到底还在不在?我遇到过几次情况,开发分支合并代码时,同事重构包名,把com.oldname.Application删了,新包名下的Application类还没建出来,结果一打包就是Unable to find main class。

检查主类是否可以很简单,先确认下面几点:

  • src/main/java目录下有没有.java文件。
  • 主类的全限定类名是否正确,比如com.example.demo.DemoApplication。
  • 类名和文件名是否一致,class是不是public的。
  • 方法签名是不是标准的public static void main(String[] args),参数名无所谓,但参数类型必须是String[]。
  • 有没有@SpringBootApplication注解。注意,这个注解其实不是让repackage识别主类的必要条件,Spring Boot插件扫描的是带main方法的类,所以就算没有这个注解,只要main方法在,插件也能扫到。真正的问题往往在于:既有项目里,有人放了一个测试用的main方法在别的类里,而真正的Application类因为缺依赖被IDE标红,或者被编译器跳过了。

如果代码层面肉眼扫不出问题,优先执行一次mvn clean compile,看看编译阶段有没有错误。如果编译都过不了,那package阶段当然也走不到最后一步repackage。

2.2 第二层:检查pom中主类相关配置

代码没问题,接下来看pom。Spring Boot Maven插件的主类有三种配置方式,我见过太多人把这三种弄混:

第一种,在<properties>里配置start-class:

<properties> <start-class>com.example.demo.DemoApplication</start-class> </properties>

第二种,在插件配置里指定mainClass:

<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.example.demo.DemoApplication</mainClass> </configuration> </plugin>

第三种,是用maven-jar-plugin的archive > manifest > mainClass配置,这个配置的是普通jar的Main-Class,和Spring Boot的Start-Class不是一回事。如果项目里同时有maven-jar-plugin的mainClass和spring-boot插件的repackage,绕来绕去容易出各种莫名其妙的问题。

排查时,优先看<properties>里的start-class。这个值一旦写错,比如包名大小写不对、类名拼错,必然报“Unable to find main class”。还有一种情况:start-class写的是正确的,但项目里根本没有这个类,也会报错。

2.3 第三层:检查编译产物target/classes

这一层非常重要,能帮你区分“源码有问题”还是“编译产物有问题”。Maven的repackage在扫描主类时,看的是target/classes目录,不是源码目录。所以不管源码里有没有Application类,只要它没有变成target/classes下的.class文件,插件就看不到。

在项目根目录执行:

find target/classes -name "*Application*.class"

Windows环境用:

dir /s /b target\classes\*Application*.class

如果能看到类似target/classes/com/example/demo/DemoApplication.class这样的文件,说明编译产物正常。如果文件不存在,问题就出在编译阶段:要么主类根本没被编译,要么主类在源码里但被编译器跳过了。

常见原因有:

  • 主类所在的源码目录结构不对,比如src/main/java被写错成src/main/javas这类,Maven不认为那是源码目录。
  • 主类被maven-compiler-plugin的excludes排除掉了。这个配置不会报编译错,但class就是不会出现在target/classes里。
  • 你用的是Kotlin或者Scala,主类的.kt或.scala文件没有被对应语言的编译插件处理,导致只有Java源码被编译,Kotlin主类根本没产出class。

其中Kotlin项目的坑我在后面的避坑章节再详细展开,因为它非常容易伪装成“Maven配置坏了”。

2.4 第四层:多模块中的插件继承关系

如果说前几层是单模块项目里最常见的排查路径,那多模块项目的情况就要多留一个心眼儿了。很多公司的业务项目是标准的“多模块+继承父pom”结构,比如:

mall-parent (pom) ├── mall-common (jar,纯工具类,无主类) ├── mall-admin (jar,启动模块,有 AdminApplication) └── mall-api (jar,提供接口SDK,无主类)

如果你在父pom里直接声明了spring-boot-maven-plugin,那么所有子模块打包时都会继承这个插件,也都会执行repackage目标。问题来了:mall-common和mall-api模块根本没有main方法,repackage一执行,直接报Unable to find main class。

这种场景在IDEA里特别迷惑人,因为默认情况下,IDEA的Maven面板会按照模块层级展示所有模块的package生命周期。你勾选父模块执行clean package,然后在mall-common模块上看到构建失败,第一反应往往是去查common模块的代码有没有问题,而真正的病根是在父pom的插件配置上。

2.5 第五层:IDE与缓存因素

最后一层是IDE相关。老实说,这个报错本身跟IDE关系不大,因为IDEA里的Maven插件和命令行Maven最终走的是同一套逻辑,同样的pom、同样的编译结果。但有一条路径会让问题看起来像是IDE导致的:IDEA自己有一套“构建项目”的编译逻辑,和Maven的编译可能不是同一次构建。

有一种很常见的现象:IDEA里点绿色三角启动项目一切正常,因为IDEA的Build Project会把源码编译到IDEA自己的输出目录(或者直接复用target/classes),而且启动时的classpath是由IDEA维护的,能识别到的类范围和Maven扫描target/classes的机制不完全一样。到了执行mvn clean package时,Maven会重新执行一次compile,如果此时源码有未保存的修改、或者IDEA的缓存导致增量编译结果异常,就可能出现“IDEA能跑、命令打包却找不到主类”的诡异情况。

遇到这种疑似缓存问题,最直接的办法就是:

mvn clean mvn compile

先clean再compile,清掉所有增量编译的残留物。很多“玄学”报错,mvn clean之后都自己好了。

3. 高频场景实战:直接复制粘贴的解决方式

3.1 单模块项目:手动指定start-class

单模块项目遇到这个报错,最省事的解法就是在pom里显式声明主类。比如你的启动类是全限定名com.example.demo.DemoApplication,直接在<properties>里加一行:

<properties> <java.version>8</java.version> <start-class>com.example.demo.DemoApplication</start-class> </properties>

这个start-class属性不是Maven的核心属性,是Spring Boot Maven插件认领的约定属性。写上这行之后,repackage优先级最高的查找路径就被“钉死”了,它不会再去扫描target/classes,也不会因为扫描到多个main方法而摇摆不定。

改完配置后,执行:

mvn clean package -DskipTests

如果还报同样的错,那基本可以断定启动类本身有问题。检查一下启动类上有没有@SpringBootApplication、有没有main方法、main方法签名是不是标准的,多数情况下这三条检查完就解决了。

这里再补充一个细节:<start-class>的值不需要加.class后缀,千万不能用com.example.demo.DemoApplication.class这种写法,插件是按类名加载的,带了后缀反而匹配不上。

3.2 多模块项目:给父模块加skip,给启动模块加mainClass

多模块项目的正解,是把repackage的执行范围严格限制在真正需要做可执行jar的启动模块上。

具体操作分两步。第一步,在父pom的spring-boot-maven-plugin配置里加上skip:

<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <skip>true</skip> </configuration> </plugin>

第二步,在真正要打包成可执行jar的子模块(比如mall-admin)的pom里重新声明这个插件,去掉skip并指定mainClass:

<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.mall.admin.AdminApplication</mainClass> </configuration> </plugin>

这个方案在模块很多的项目里最干净,不会影响其他模块的jar产出。mall-common和mall-api模块因为没有重新声明插件,依然会继承父pom的skip配置,repackage直接跳过。

如果你不喜欢在子模块里重新声明插件的写法,也可以让父pom里只声明插件不写skip,然后在父pom的<modules>里不对非启动模块做任何特殊处理,这种情况下,非启动模块的打包行为就会退回到未绑定repackage的状态。不过现实中很多脚手架已经习惯了统一在父pom声明插件,所以<skip>true</skip>是更稳妥的兜底方案。

3.3 多个main方法:如何显式声明“谁才是启动类”

还有一类项目,代码里不止一个main方法。最常见的是工具类里为了本地测试写了一个main方法,Application类又有自己的启动main方法。这时候Spring Boot插件扫描到多个main方法,行为在不同版本里会有差异:有的版本直接报错,有的版本会选第一个遇到的类作为主类并打出一个警告。

不管是哪种情况,这种“让插件猜”的做法都不可取。万一插件猜中的是那个工具类,repackage倒是成功了,但最终jar启动时会抛Error: Main method not found(如果工具类的main签名不标准),或者启动后根本不是Spring Boot应用。

所以只要模块里存在多个带main方法的类,我建议直接把start-class写进properties:

<properties> <start-class>com.example.order.OrderApplication</start-class> </properties>

这样无论插件扫到多少个main方法,它都会无条件听从这个配置。

3.4 纯工具模块:让repackage安静跳过

另一个很容易踩坑的场景,是一个模块本身不负责启动,它只是给别人提供依赖的SDK或工具包。比如你有一个xxx-core模块,里面全是POJO、工具类、Feign接口,没有main方法,也不需要被java -jar启动。

这种模块如果继承了父pom里绑定了repackage的spring-boot-maven-plugin,打包时大概率就是Unable to find main class。

最直接的解法就是前面说的skip:

<configuration> <skip>true</skip> </configuration>

加了skip之后,repackage目标会被直接跳过,这个模块生成的jar就是一个普普通通的jar,可以被其他模块通过依赖正常引用。很多人可能担心“skip了会不会影响依赖打包”,完全不会。Spring Boot插件只在repackage目标上起作用,它对普通jar的依赖传递没有任何影响。

4. 冷门原因实战:日志、版本与脚手架里的坑

4.1 用DEBUG日志定位主类扫描过程

前面讲的都是“按经验猜”,其实还有更硬核的定位方式:让Maven把调试日志打出来,看repackage在主类扫描时到底干了什么。

执行打包时加一个-X参数:

mvn clean package -DskipTests -X

日志会非常多,建议输出到文件里:

mvn clean package -DskipTests -X > build.log 2>&1

然后用编辑器搜索关键词main class、repackage,配合包名快速定位。在调试日志里,你能看到插件在哪个目录下扫描class、找到了几个带main方法的类、最后用的是哪一个类。比如Spring Boot 2.x下会出现类似:

[DEBUG] Searching for main class... [DEBUG] Adding classes to repackaged jar...

虽然不同版本打印的信息不一样,但只要把日志定位到插件执行的那一段,主类扫描的来龙去脉基本就清楚了。这个方法比瞎试配置要快得多,尤其适合定位那种“配置看着都对,但就是报错”的诡异问题。

4.2 插件版本缺失或与Spring Boot版本错配

Spring Boot Maven插件的版本,其实默认可以跟着spring-boot-starter-parent的版本走。如果项目使用了自定义父pom,没有继承spring-boot-starter-parent,那这个插件就必须显式写版本。

我就见过一次,项目的pom里这样配置:

<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin>

没写版本。如果Maven配置里还配了镜像仓库或者本地仓库缓存了一些旧版本的插件元数据,会默认拉到某个很老的插件版本,比如1.4.x。这个老版本去找主类时,可能就不会读取<start-class>这种新约定,行为随之变得不可控,最后报了Unable to find main class。

正解很简单,给插件补一个和Spring Boot版本匹配的版本号:

<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.7.18</version> </plugin>

Spring Boot 3.x对应插件版本就是3.x,2.x项目对应2.x,建议插件版本和Spring Boot版本保持一致。

4.3 compiler插件excludes把主类过滤掉

这个坑相对冷门,但一旦踩上特别费解。事情是这样的:有些项目为了做代码分层,会在maven-compiler-plugin里配置excludes,把某些包下的源码排除在编译之外。这种配置本意是排除测试代码或自动生成的代码,结果有人把**/*Application*.java这种规则写了进去,或者把主类所在的包名写进了excludes,主类直接被“编译跳过”。

现象就是:源码目录里Application类明明在,但target/classes下根本没有对应的class文件,repackage自然找不到主类。

排查方法就是前面说的,先去看target/classes下到底有没有主类的class文件。如果没有,再翻翻maven-compiler-plugin的配置,看excludes里是不是把主类所在路径匹配掉了。如果是,把excludes规则改正确,或者干脆把主类从excludes里放出来。

4.4 主类“没资格”:不是public或签名不对

还有一种极其隐蔽的情况:主类文件在源码里,target/classes下也有class文件,但repackage依然找不到它。这个锅往往出在main方法的签名上。

Java的main方法签名必须是:

public static void main(String[] args)

这里有几个关键点:方法必须是public,必须是static,返回类型必须是void,参数必须是一个String[],不能是String...(虽然变长参数本质上也是String[],但严格按反射时需要区分)。

还有一种情况是主类本身不是public的,比如写成了class Application而不是public class Application。这种写法编译不会报错,生成的class文件也存在,但作为一个可执行jar的启动入口是不合格的。Spring Boot插件在扫描时不一定会把这种类判定为有效主类,就算它通过了扫描,最后java -jar运行时也可能出现:“Error: Main method not found in class xxx”。所以主类的public修饰符一定不要省。

4.5 脚手架pom残留配置引发的假报错

最后一个冷门原因,是公司内部脚手架或者网上生成的项目自带了一些“看起来有用”的配置,但实际运行时成了干扰项。

我在一个老项目里就遇到过,pom文件中同时存在maven-shade-plugin和spring-boot-maven-plugin。两个插件都会去改MANIFEST.MF,执行顺序一乱,repackage阶段读到的jar内容已经被shade插件加工过,主类信息混乱,最后就变成Unable to find main class。

这种场景下,最简单的处理方式是确认一下项目中到底需不需要shade插件。如果你的目标是Spring Boot可执行jar,用官方推荐模式就可以:spring-boot插件负责repackage,shade插件可以删掉,或者把它绑定到其他执行阶段。除非你的场景必须用shade做资源合并,否则真的没有必要让两个插件同时处理同一个jar。

另外,有些脚手架会在pom里写一个空壳的<mainClass>配置,但实际项目换过包名之后忘了改,这个值可能是旧项目的路径。只要这个配置存在,repackage就会优先读它,一样会报找不到主类。所以排查时,插件配置里的mainClass值是重点检查项。

5. 常见问题速查表与实战心得

5.1 报错现场与最快解法对照

整理了一份速查表,方便大家按图索骥:

报错/现象可能原因最快解决办法
单模块打包报Unable to find main class,启动类存在spring-boot插件扫描不到主类或start-class配置错误在<properties>中显式配置<start-class>为启动类全限定名
多模块中某个非启动模块报错插件被父pom继承,子模块无主类父pom插件配置加<skip>true</skip>,启动模块单独声明插件
代码里存在多个main方法插件扫描到多个候选,选错类用<start-class>或插件<mainClass>显式指定
源码有Application类,target/classes下没有对应class编译阶段被跳过或排除检查maven-compiler-plugin的excludes配置
IDEA能启动,命令行打包失败编译增量或缓存问题执行mvn clean compile,清除旧编译结果
插件没写版本号,或版本与Spring Boot不匹配插件行为异常显式指定与Spring Boot版本一致的插件版本
有main方法但签名不规范插件判定为非有效主类修成public static void main(String[] args)
项目同时用了shade插件和spring-boot插件两插件冲突,MANIFEST被改乱去除shade插件或调整执行阶段

这张表覆盖了我这几年遇到的大多数情况,基本能解决九成以上的Unable to find main class问题。

5.2 一些维护建议

聊完了具体解法,再分享几个从这些坑里提炼出来的习惯。

第一,多模块项目里,一定不要让父pom的spring-boot-maven-plugin在所有子模块上执行repackage。这不是“偷懒不配置”,而是职责划分问题。启动模块做可执行jar,公共模块做普通jar,两者本来就不应该用同一个模板约束。

第二,编写一个项目时,把<start-class>当作模块的元数据来维护。就像版本号一样,它应该在模块创建时就固定下来,不要等到打包报错才想起来补。团队内可以约定:所有Spring Boot启动模块的pom里,必须在properties区域有start-class,没有就是不合格配置。

第三,遇到这种报错,先看target/classes再看pom,不要一上来就怀疑Maven本身。Maven再复杂,本质上也是“源码——编译——打包”的流水线,流水线卡住的位置总会在日志里留下线索。mvn clean package -X虽然是笨办法,但往往是最快让你看到真相的办法。

我个人在实际操作中的体会是,这类打包问题绝大多数都不是Maven坏了,而是“插件不知道入口在哪”或“入口没被编译出来”这两类问题。只要思路清晰,按照从源码到产物、从配置到插件的顺序排查,通常五到十分钟就能定位。以后如果再看到这个红字,不妨先别急着搜索复制粘贴,自己按这条链路走一遍,解决起来会顺手很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询