news 2026/10/1 12:33:50

Maven打包报错Unable to find main class:从根源到修复的全面指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Maven打包报错Unable to find main class:从根源到修复的全面指南

先说明一个很常见的尴尬场景:你在 IDEA 里写好了一个 Spring Boot 项目,Application类里的main方法写得明明白白,点击 Maven 面板里的package,控制台却甩出这么一行:

[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.7.5:repackage (repackage) on project demo: Execution repackage of goal org.springframework.boot:spring-boot-maven-plugin:2.7.5:repackage failed: Unable to find main class

中文翻译过来就是“打包阶段重打包失败,找不到主类”。但你心里很清楚:主类就在那儿,明明昨天还用同样的方式打出来了 jar。问题到底出在哪?

这个报错在 Maven 使用者的日常里出现频率极高,几乎每个做 Spring Boot 多模块项目的人都至少碰过一两次。它牵扯到的不仅仅是“你有没有写 main 方法”,而是 Maven 构建顺序、插件继承、编译产物目录、JDK 版本等多个层面的东西。这篇文章我就从底层机制开始,把这个报错彻底拆开,再按不同根因给出对应的修复方案,最后复盘一次完整的多模块排查过程。

1. 先还原报错现场:这条“Unable to find main class”从哪一步冒出来

老规矩,先看报错的位置。整条报错的核心是spring-boot-maven-plugin:2.7.5:repackage,注意这里的repackage是插件的 goal,不是 Maven 的package生命周期。整个流程是这样的:

Maven 执行mvn clean package时,package阶段会把项目编译后的 class 文件打成普通的 jar 包。打完这个普通 jar 后,如果pom.xml配置了spring-boot-maven-plugin的repackage目标,插件会紧接着对这个 jar 进行“二次加工”,把它转换成 Spring Boot 的可执行 JAR。

这个二次加工发生在同一个 package 阶段里,顺序大致是:

compile -> test -> jar -> repackage

所以你在日志里通常会先看到spring-boot-maven-plugin ... repackage,然后才看到失败信息。报错的触发点非常明确:repackage 在处理已经生成的 jar 时,没能在里面找到适合作为应用入口的主类。

这就出现了一个认知上的常见偏差:很多人以为“找不到主类”指的是编译器找不到,去查源码、查目录结构,其实编译器早就把源码编译成 class 了,问题出在 later 的重打包阶段。

先记住一句话:这个错误跟你的源码“里有没有” main 方法没有必然关系,它关心的是插件有没有能力从构建产物中“提取到”一个合法的启动类。源码里有,不代表插件一定能认出来。

另外,这个报错并不是只有命令行执行mvn package才会出现。你在 IDEA 的 Maven 面板里双击package、在 CI 脚本里跑构建、甚至某些开发者在配置 Docker 镜像构建流程时启用 Maven 打包步骤,都可能踩到完全相同的一行错误。报错虽然只有一句话,背后对应的项目状态却可能五花八门,下面我们就先把 repackage 的查找逻辑彻底讲透。

2. repackage 的查找机制:Maven 是怎么在 class 文件里“找”主类的

要解决这个报错,就必须知道 repackage 到底怎么找主类、按什么规则判断一个类能不能当主类。

Spring Boot 的可执行 JAR 和普通 JAR 有一个核心区别:普通 JAR 只是把项目自身编译后的 class 文件和资源文件压缩在一起,而 Spring Boot 可执行 JAR 需要让java -jar能直接启动整个应用,所以它必须额外写入两类信息:

Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: com.example.demo.DemoApplication

Main-Class是 JVM 启动时的真正入口,Spring Boot 会用自己的JarLauncher来负责加载嵌套在 BOOT-INF/lib 里的依赖 jar;Start-Class才是你自己写的那个带 main 方法的应用类。repackage 过程中,插件必须先拿到Start-Class的完整类名,才能把它写进 MANIFEST.MF,否则这个可执行 JAR 就缺少启动入口。

插件获取主类名的顺序也很关键:

  1. 先看pom.xml里 spring-boot-maven-plugin 的<configuration>中是否显式配置了mainClass。如果配置了,插件直接用这个类名,不再扫描。
  2. 如果没有配置,插件会自动扫描target/classes目录下的所有.class文件。
  3. 扫描规则是找public static void main(String[] args)方法,方法名必须是main,参数必须是String[],修饰符必须是public static void,这个匹配是严格到字节码级别的。
  4. 找到符合条件的类,作为Start-Class写入清单文件。

这里有几个非常典型的反例:

  • 有人把main方法写成了public void main(String[] args),少了static,编译不会报错,但插件不认。
  • 有人写的是public static void main(String args),参数不是数组,同样编译能通过,插件还是看不出这是入口。
  • 有人把主类写在了没有public修饰的类里,或者把 main 方法放进了一个普通工具类中,识别阶段也可能失败。

插件在扫描阶段找不到可以匹配的类,就会抛出Unable to find main class。同时,如果模块里存在多个 main 方法,不同版本的处理方式也不一样,有的版本会选第一个,有的版本会直接抛异常。但无论哪种情况,工程实践上都不该依赖“碰运气”让它随便选一个当入口,正确做法永远是显式声明主类,或者保证模块里只有一个可执行的 main 方法。

所以问题的本质可以归纳成一句话:repackage 需要一个明确的启动入口,要么你告诉它在哪,要么它自己能在编译产物里精准地找到唯一一个合法的入口。

3. 按根因分类修复:从改配置到重新设计模块结构

了解了查找机制后,再去看各种报错场景就会清晰很多。下面按照实际项目里最常见的几类根因,给出对应的解决方案。

3.1 应用模块的主类没被插件识别:先显式声明 mainClass

最直接的情况:项目本身是一个 Spring Boot 应用,主类也有,但因为某些原因插件扫描不到,这时候最省事的办法就是在pom.xml里显式指定主类。

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

如果你使用了spring-boot-starter-parent作为父工程,还有一个更偷懒的方式,是通过属性指定start-class:

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

这里要注意一个细节:start-class是 Spring Boot 定义的属性名,在spring-boot-starter-parent里被声明成了可配置项;而<configuration><mainClass>是 spring-boot-maven-plugin 插件自己的配置参数。两者都能影响最终Start-Class的写入,但作用范围略有不同。项目中如果同时出现两者且值不一致,以插件配置里的mainClass为准。

显式声明的另一个好处是:如果模块里恰好有多个 main 方法,不管插件版本的处理策略是什么,你都可以锁定真正的启动类,彻底避免插件“选错入口”带来的一系列奇怪问题。

3.2 纯库模块被误加了打包插件:用 skip 参数跳过重打包

这个场景在微服务项目里特别常见。比如你的工程里有common、dal、utils这类基础模块,它们内部根本没写 main 方法,也不可能单独启动。但如果你在父工程的<build><plugins>里直接声明了spring-boot-maven-plugin,这个插件会被所有子模块继承,结果是每一次构建到这些普通 jar 模块时,repackage 都会去扫描主类,扫不到就直接报错。

解决办法有两种,选哪一种取决于你的工程结构:

如果你只希望某个模块不触发 repackage,就在该模块的插件配置里加上skip:

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

如果整个父工程下面的子模块只有少数几个是可启动应用,我更推荐的做法是把插件从父工程的<plugins>挪到真正的应用模块中去。这样普通模块连插件都不会继承,自然也不会执行 repackage,省得为每个库模块都写一遍 skip。

<!-- 父 pom 中只做版本管理 --> <build> <pluginManagement> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </pluginManagement> </build> <!-- 仅应用模块 pom 中声明使用 --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>

这里特别提醒一句:不要以为删除普通 jar 模块里的 main 类就能解决,真正的问题是“一个不需要可执行 jar 的模块根本不该执行 repackage 这个动作”。你可以在mvn package之后去看 target 目录下生成的 jar 文件大小,如果模块里没有任何依赖但突然冒出一个几十 MB 的 fat jar,那基本可以确定是 repackage 在这个模块上也生效了。

3.3 多模块依赖顺序和父工程插件继承引起的“连锁反应”

再来看一个更容易混淆的情况:多模块项目里,应用模块 A 依赖普通模块 B,B 模块自己又因为继承了上面的插件而需要执行 repackage。构建时 Maven 会先构建 B,B 构建到 package 阶段就报Unable to find main class,导致整体构建中断。很多人第一反应是去查 A 模块的主类,查了半天才发现报错信息里写的是 B 模块。

遇到这种情况,最有效的方式是第一时间定位报错中的on project xxx字段。比如:

[ERROR] Failed to execute goal ... on project common: ...

这个common才是要处理的对象。然后判断它到底是不是一个需要被打成可执行 JAR 的模块:不是,就按前面 3.2 节的思路处理;是,才进入主类排查的流程。

还有一种比较隐蔽的坑:有些模块本身是已经打好的可执行应用,但依赖方把它当成普通 jar 引进来。这种情况项目会莫名其妙地多出一个“启动另一个应用”的副作用,同时如果同一个仓库里有多个可执行模块,构建顺序也可能不稳定。规范做法是:多模块仓库中,保证只有最顶层的应用子模块是可执行的,其余模块一律按普通 jar 处理。

3.4 JDK 版本不匹配引发的“伪报错”

这个坑很少有人第一时间想到,但它真实存在。Spring Boot 插件在用字节码方式扫描 class 文件时,需要能正常读取 class 文件结构。如果你的 Maven 运行在一个 JDK 版本,而项目编译参数又指定了另一个版本,或者编译器产出的 class 版本过高,插件读取阶段就可能出现底层异常,偶尔会连带表现为Unable to find main class。

最常见的场景是:

  • 本地JAVA_HOME是 JDK 17,项目pom.xml里java.version写的是 8,但maven-compiler-plugin没有正确设置release参数。
  • 编译产物实际是 JDK 17 的 class 文件,而 Spring Boot 2.x 某个版本的 repackage 读取这些 class 时出现兼容问题。

所以遇到这个报错,不要光盯着主类,顺手检查一遍环境也是分内的事:

java -version mvn -version

mvn -version输出里能看到 Maven 当前使用的 Java 版本。如果它和java.version相差过大,先统一版本再重新打包试试。这里我建议在pom.xml里明确加上 compiler 的release参数:

<properties> <java.version>17</java.version> <maven.compiler.release>17</maven.compiler.release> </properties>

maven.compiler.release的作用是告诉编译器,生成的 class 文件必须符合对应 JDK 版本的规范,这样比只设置source/target更严格,也能避免一些跨版本编译导致的问题。

4. 命令行 + IDEA 双路径排查:一次多模块实际踩坑过程演示

很多朋友遇到这种报错习惯直接搜解决方案,但我更建议你先花五分钟把问题定位清楚。下面是我自己用的排查流程,基本能覆盖 90% 的情况。

第一步,先看报错里的模块名。命令行执行完整构建时,注意on project xxx后面的值。如果这个模块根本不该启动,直接加skip=true或移除插件即可。

第二步,进入报错模块,查看编译产物中是否存在主类。先执行mvn clean compile,然后确认 class 文件是否生成:

ls target/classes/com/example/demo/DemoApplication.class

如果没有这个文件,说明源码编译或目录结构有问题,比如src/main/java路径被错误修改、源码被 IDE 排除了编译范围等。从 Git 最近改动里对比一下,往往很快能找到原因。

第三步,class 文件存在,继续用javap验证 main 方法签名:

javap -p -classpath target/classes com.example.demo.DemoApplication

正常情况下输出里有这么一行:

public static void main(java.lang.String[]);

如果看到void main(java.lang.String[]),缺少public static,那插件当然不认。如果看到的是泛型或别的参数类型,同样不符合要求。

第四步,如果这些都没有问题,再检查是不是多模块共用父 pom 导致的插件继承问题。最直观的方式是单独进到应用模块目录,执行:

mvn package -pl demo -am

然后观察是不是只构建应用模块本身时也会报错。如果单独构建成功、整体构建失败,那就是模块之间的插件继承关系在捣乱。

在实际排查中,我还会经常用到一个快速判断方式:直接解压打出来的普通 jar 看看有没有主类声明。因为 repackage 是在普通 jar 的基础上处理的,如果生成的原始 jar 里 MANIFEST.MF 都没有Main-Class,那 repackage 自然无从下手。

在 IDEA 里排查的时候,有两点容易踩:

第一,IDEA 自身的 Build 和 Maven 构建不是一回事。如果你在 IDEA 里先执行了 Build Project,IDEA 会把 class 输出到out/目录,但 Maven 打包用的是target/目录。这时候最好先双击 Maven 面板的clean,再执行package,避免老旧的目标文件干扰判断。

第二,IDEA 对 Maven 项目的自动导入偶尔会“假成功”。如果pom.xml改了插件配置但没有触发 Maven reload,实际生效的仍是旧的依赖关系。看到 IDEA 右下角提示Maven projects need to be imported时,点一下刷新再试,这个细节省时省力。

为了更直观地对照问题根因和解决方案,我把常见的几种情况整理成了表格:

报错场景根因最直接的处理
单模块 Spring Boot 应用,主类存在但报错插件没扫描到主类显式配置 mainClass / start-class
普通工具模块继承打包插件不需要 repackage加 skip 参数或移除插件
多模块整体构建失败,报错模块不是应用模块模块被错误地执行了 repackage将插件声明限制到真正的应用模块
主类存在且签名正常,仍扫描失败JDK 版本或编译产物不兼容统一 JAVA_HOME 和 java.version,clean 后重打
主类签名不正确main 方法缺 static 或参数错误修正 main 方法签名
target 目录旧文件残留重复构建导致元数据混乱先执行 mvn clean 再 package

表格里的这些路径基本覆盖了我见过的绝大多数情况,剩下的属于插件自定义程度特别高的项目,那就需要去对照 spring-boot-maven-plugin 的官方文档里mainClass、skip、classifier这几个参数的具体语义了。

5. 避开这块“第 N 次踩坑的感受和之后的习惯”

入行这么多年,我用 Maven 打包踩过的坑里有三分之一都和 repackage 有关。第一次碰到这个报错的时候,我还应当是在给一个老旧的 Spring Boot 1.x 项目升版本,彼时对“主类明明存在却被说找不到”的困惑相当大。后来一步步翻源码、对照文档,才算把这个机制理顺。

这里分享几个我自己固定的例行习惯,能大大减少踩坑概率:

习惯一:每个模块的职责要清晰。同一个 Maven 仓库里,我会严格区分“可执行应用模块”和“被依赖的库模块”。库模块的 pom 里不继承 spring-boot-maven-plugin,除非它确实需要被打成可执行 jar。这样的结构从源头杜绝了库模块触发 repackage 的可能。

习惯二:不在父 pom 的 plugins 里声明会执行 repackage 的插件。父 pom 只放 pluginManagement,负责锁定版本和默认配置,具体要不要启用,留给各个子模块自己决定。这招对多模块项目的可维护性提升非常明显。

习惯三:给应用模块显式声明 mainClass。即使当前模块里只有一个 main 方法,我也会写上。因为项目后期大概率会加入一些带 main 的调试工具类、批处理入口,到时候多入口并存,不写 mainClass 的话插件行为就不稳定,运气不好就会打包出一个错误入口的可执行 jar。

习惯四:每次构建前先 clean。这个听起来像是在说废话,但实际项目中因为 target 目录残留旧 class 文件导致的诡异问题,我至少见过十次以上。Maven 构建不是 IDE 增量编译,不 clean 的情况下,某些被删除的类可能在旧 jar 里继续存在,干扰 repackage 的判断。

最后再说一个很多人不知道的小点:如果你确实不需要 repackage,又不想改太多配置,可以用classifier方式绕过:

<configuration> <classifier>exec</classifier> </configuration>

它会额外生成一个xxx-exec.jar作为可执行包,原始 jar 则保持普通库 jar 的形态。这样你的模块既能被别人当依赖引用,也能独立启动。不过,这个方案对团队协作不那么友好,因为命名变了之后,依赖方引用的坐标可能也要跟着调,所以不是特殊情况,我一般还是会优先选择按职责拆分模块和 skip 方案。

Maven 的报错信息有时候很直接,有时候又特别容易让人绕远路,Unable to find main class就属于后者。它表面在说“没找到主类”,实际上大部分时候是“构建配置没有正确告诉插件主类在哪里”。理解了 repackage 的工作方式,再按报错模块、编译产物、插件配置、JDK 版本这个顺序排查,问题基本都能在一个小时内解决。下次再看到这行红色报错,你已经知道它真正想说的是什么了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 12:33:43

Java实现琴房预约管理系统:需求拆解与并发控制实战

做毕业设计这些年&#xff0c;最常被问到的一道题就是“老师&#xff0c;琴房预约这种题能做吗&#xff1f;感觉业务太简单&#xff0c;怕写不出东西”。其实恰恰相反&#xff0c;琴房预约管理系统是高校里特别典型的管理类题目&#xff0c;需求边界清晰、角色分明、有并发冲突…

作者头像 李华
网站建设 2026/10/1 12:33:39

Stream流全拆解:概念、排序实战与disconnected报错排查

最近排查一个线上问题时&#xff0c;日志里连续出现几行这样的报错&#xff1a;stream disconnected before completion: stream closed before response.completed、transport error: network error: error。说实话&#xff0c;这类报错看起来简短&#xff0c;但背后牵涉的东西…

作者头像 李华
网站建设 2026/10/1 12:33:36

中小企业GPU算力租赁预算指南:从选型到成本优化

中小企业的AI算力预算&#xff0c;十有八九是笔糊涂账。我见过太多团队&#xff0c;一上来就问"租一张4090一个月多少钱"&#xff0c;然后按最便宜的单价下单&#xff0c;结果跑了两周发现显存不够、卡型不匹配、数据传输比训练还慢&#xff0c;钱花了活没干成。算力…

作者头像 李华
网站建设 2026/10/1 12:31:42

大数据与LLM融合的农产品价格预测及推荐系统实战

前几年做毕业设计&#xff0c;大家还在纠结SSH框架还是Spring Boot&#xff0c;今年风向已经完全变了——别再只做一个CRUD管理系统了。如果你选了大数据方向&#xff0c;又想蹭上大模型的热度&#xff0c;这个选题值得认真看一下&#xff1a; Spark Hadoop Hive LLM大模型…

作者头像 李华
网站建设 2026/10/1 12:30:44

XGBoost原理与实战:从梯度提升树到调参优化全攻略

第一次用XGBoost的时候&#xff0c;我其实挺烦它的——默认参数跑下来&#xff0c;准确率确实还行&#xff0c;但换个数据集效果就飘忽不定&#xff1b;调max_depth和learning_rate全凭感觉&#xff0c;加正则化更是一头雾水。后来把算法原理啃了一遍&#xff0c;再回头做项目&…

作者头像 李华
网站建设 2026/10/1 12:30:41

团队协作信任法则:可预期性、责任边界与反馈闭环

1. 为什么谈“信任”比谈“努力”更重要在职场里泡久了&#xff0c;你会发现一个现象&#xff1a;能力强的团队不一定赢&#xff0c;但彼此信任的团队很少输。我过去带项目、做跨部门协作、和外包团队打交道&#xff0c;最深的体会是——信任不是一种性格魅力&#xff0c;而是一…

作者头像 李华