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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration skiptrue/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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration skiptrue/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里。如果是后者再逐一查看各子模块是否显式配置了skiptrue/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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent这样插件版本自动对齐省心。5.2mainClass和start-class的关系有读者可能见过另一种配置方式是在 pom 的properties里设置start-classproperties start-classcom.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 classmain 方法签名非法检查是否为public static void main(String[] args)多模块项目中报错模块没有主类插件在纯工具模块中执行了 repackage在公共模块显式配置skiptrue/skip报错模块没有配置任何插件父 pom 插件声明被继承将插件从plugins移到pluginManagement启动类存在但插件仍找不到启动类不在当前模块的编译范围内调整项目依赖关系让启动类所在模块作为可执行模块打包成功但运行时报错找不到主类repackage 未真正执行检查是否有多个 spring-boot-maven-plugin 定义导致执行顺序异常所有配置看起来都正常但仍报错本地 Maven 仓库缓存了旧插件配置执行mvn clean package -U强制更新快照这张表是我在实际排查中最常用到的很多问题本质上是同一个病因的不同表现。记住一个原则遇到 repackage 报错优先怀疑项目结构和插件配置而不是业务代码能帮你省下大量排查时间。最后再分享一个我自己的小习惯在项目的主 pom 里我通常会为所有模块统一设置一个属性properties java.version1.8/java.version project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties这不是直接解决Unable to find main class的方法但能够保证所有模块编译参数一致避免因为编译版本不统一导致的次生问题。毕竟很多看上去很离谱的报错背后可能只是某些环境差异被放大了。