ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot Maven打包报错:Unable to find main class排查指南

Spring Boot Maven打包报错:Unable to find main class排查指南 “repackage failed: Unable to find main class”看到这行红字的时候很多人的第一反应是——我的Application类明明写得好好的凭什么说找不到别急这个报错我前前后后遇到过不下十次每次的原因还都不太一样。尤其在用Maven打包Spring Boot项目时这条报错出现的频率高得惊人而且经常在项目本机IDEA里跑得好好的一到命令行mvn clean package就翻车。这个报错本身并不复杂就发生在Spring Boot Maven插件执行repackage目标的时候插件要重打一个可执行jar但扫描完编译产物后没找到任何可以作为启动入口的main方法。插件找不到“开机按钮”自然拒绝继续往下走。文章后面我会把背后的原理、排查顺序、高频场景、以及一些常规文档里不会写的冷门坑全部整理出来无论是第一次遇到这个报错的新手还是被它反复折磨过的老手都能在这里找到对应的解法。1. 先弄明白repackage到底是什么它为什么非要找一个main class1.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-classproperties start-classcom.example.demo.DemoApplication/start-class /properties第二种在插件配置里指定mainClassplugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.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*.classWindows环境用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.version8/java.version start-classcom.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配置里加上skipplugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration skiptrue/skip /configuration /plugin第二步在真正要打包成可执行jar的子模块比如mall-admin的pom里重新声明这个插件去掉skip并指定mainClassplugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.mall.admin.AdminApplication/mainClass /configuration /plugin这个方案在模块很多的项目里最干净不会影响其他模块的jar产出。mall-common和mall-api模块因为没有重新声明插件依然会继承父pom的skip配置repackage直接跳过。如果你不喜欢在子模块里重新声明插件的写法也可以让父pom里只声明插件不写skip然后在父pom的modules里不对非启动模块做任何特殊处理这种情况下非启动模块的打包行为就会退回到未绑定repackage的状态。不过现实中很多脚手架已经习惯了统一在父pom声明插件所以skiptrue/skip是更稳妥的兜底方案。3.3 多个main方法如何显式声明“谁才是启动类”还有一类项目代码里不止一个main方法。最常见的是工具类里为了本地测试写了一个main方法Application类又有自己的启动main方法。这时候Spring Boot插件扫描到多个main方法行为在不同版本里会有差异有的版本直接报错有的版本会选第一个遇到的类作为主类并打出一个警告。不管是哪种情况这种“让插件猜”的做法都不可取。万一插件猜中的是那个工具类repackage倒是成功了但最终jar启动时会抛Error: Main method not found如果工具类的main签名不标准或者启动后根本不是Spring Boot应用。所以只要模块里存在多个带main方法的类我建议直接把start-class写进propertiesproperties start-classcom.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。最直接的解法就是前面说的skipconfiguration skiptrue/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 21然后用编辑器搜索关键词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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin没写版本。如果Maven配置里还配了镜像仓库或者本地仓库缓存了一些旧版本的插件元数据会默认拉到某个很老的插件版本比如1.4.x。这个老版本去找主类时可能就不会读取start-class这种新约定行为随之变得不可控最后报了Unable to find main class。正解很简单给插件补一个和Spring Boot版本匹配的版本号plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId version2.7.18/version /pluginSpring Boot 3.x对应插件版本就是3.x2.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插件负责repackageshade插件可以删掉或者把它绑定到其他执行阶段。除非你的场景必须用shade做资源合并否则真的没有必要让两个插件同时处理同一个jar。另外有些脚手架会在pom里写一个空壳的mainClass配置但实际项目换过包名之后忘了改这个值可能是旧项目的路径。只要这个配置存在repackage就会优先读它一样会报找不到主类。所以排查时插件配置里的mainClass值是重点检查项。5. 常见问题速查表与实战心得5.1 报错现场与最快解法对照整理了一份速查表方便大家按图索骥报错/现象可能原因最快解决办法单模块打包报Unable to find main class启动类存在spring-boot插件扫描不到主类或start-class配置错误在properties中显式配置start-class为启动类全限定名多模块中某个非启动模块报错插件被父pom继承子模块无主类父pom插件配置加skiptrue/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坏了而是“插件不知道入口在哪”或“入口没被编译出来”这两类问题。只要思路清晰按照从源码到产物、从配置到插件的顺序排查通常五到十分钟就能定位。以后如果再看到这个红字不妨先别急着搜索复制粘贴自己按这条链路走一遍解决起来会顺手很多。
返回列表