
1. 问题现象一条报错引发的连锁排查先还原一下现场。你在控制台里输入了java -jar yourapp.jar满心期待服务启动结果屏幕上干干净净地甩出一行红字no main manifest attribute, in yourapp.jar翻译成人类语言就是这个jar包里找不到主清单属性我不知道该从哪里启动程序。这里先解释一个概念。jar包本质上是一个zip压缩包里面除了你编译好的.class字节码文件还会有一个META-INF/MANIFEST.MF文件这个文件就是jar包的“身份证”和“说明书”。当你在命令行执行java -jar xxx.jar时JVM会先读MF文件找到Main-Class这一项才知道该执行哪个类的main方法。如果MF文件缺失、损坏、或者Main-Class指向的类不存在就会报这个错。这个报错有个让人头疼的地方它在开发环境里经常不出现因为你用IDEA或Eclipse跑程序时IDE会帮你自动指定入口类但打成jar包放到服务器或别人的机器上用纯命令行启动问题就暴露了。换句话说这不是编译错误而是打包配置错误。我自己第一次遇到这个报错是在一个Spring Boot项目上。当时用IDEA的clean package正常打出了jar包本地双击能跑结果拷到Linux服务器上一运行就报这个。后来排查才知道是IDEA打包时没走maven插件用了自带的Artifact打包方式导致生成的MANIFEST.MF里压根没有Main-Class项。这种坑如果没有踩过一次光靠搜报错是很难定位到根因的。如果你也正在被这个报错折磨别急。这篇文章会从“为什么报错”到“怎么修”给你梳理出一套可以直接照抄的排查方案覆盖Spring Boot项目、普通Java项目以及一些比较刁钻的特殊场景。2. 搞懂根因MANIFEST.MF、Main-Class 和启动原理2.1 一个jar包的内部构造在动手解决之前建议你先花两分钟把jar包的结构彻底搞明白因为后面所有排查手段都是围绕这几个文件和属性展开的。用解压软件打开一个可正常运行的jar包你会看到类似这样的结构yourapp.jar ├── META-INF/ │ └── MANIFEST.MF ├── com/ │ └── example/ │ └── YourMainClass.class └── (其他资源和依赖可选的BOOT-INF目录)MANIFEST.MF是一个纯文本文件它的内容长这样Manifest-Version: 1.0 Created-By: Maven Jar Plugin 3.2.0 Main-Class: com.example.YourMainClass特别留意最后一行Main-Class。这一行的值就是你的入口类类名必须是全限定名带包名而且该类必须包含一个标准签名的主方法public static void main(String[] args)JVM执行java -jar时本质上就是去找Main-Class的值然后加载这个类调用它的main方法。如果找不到就抛出开头那个 no main manifest attribute 错误。2.2 为什么开发环境不报错一打包就报错这个现象在很多新手眼里特别诡异。其实原因不复杂开发环境运行时比如IDEA里直接点绿色三角形IDE是通过分析你的项目结构来定位入口类的它根本不需要读取MANIFEST.MF而java -jar的方式必须依赖MF文件。两者走的是完全不同的路径。还有一点需要留意IDEA自带的打包方式和Maven的打包方式生成MANIFEST.MF的策略完全不同。IDEA的Artifact打包默认不会生成Main-Class属性除非你在配置里手动指定而Maven的maven-jar-plugin如果配置了mainClass会帮你写进去。所以同一个项目用不同方式打包一个能跑一个不能跑这不是你的代码有问题纯粹是打包配置问题。2.3 不同类型的jar包处理方式完全不同再补充一个很多人忽略的常识。根据项目类型的不同同样的“主清单属性缺失”错误解决方案可能完全不一样Spring Boot项目用的是Spring Boot的打包插件它会生成一个可执行jarfat jar里面除了你的代码还把Tomcat、Spring框架等所有依赖都塞进去了。这类jar的MANIFEST.MF里不仅要有Main-Class还要有Start-ClassSpring Boot真正启动类和Spring-Boot-Version等属性。如果你直接用的是普通Maven打包而不是Spring Boot插件同样报这个错。普通Java项目非Spring Boot只依赖极少量的第三方库。这类jar的MF文件只需要Main-Class就够了但如果你引用了第三方依赖打包时必须把依赖一并处理否则运行时会报ClassNotFoundException。纯工具类jar包这个jar本身没有可执行的入口只是一个被其他项目引用的库。这种jar对它调用java -jar本来就是错误用法所以报“主清单属性缺失”是正常的不需要修。搞清楚这些区别之后下面几节的解决方案就好理解了。3. Spring Boot 项目的标准解法3.1 第一步检查你的pom.xml如果你用的是Spring Boot项目90%的“主清单属性”问题出在pom.xml缺失打包插件。先打开pom.xml找到build节点确认有没有这一段build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId version2.7.18/version /plugin /plugins /build注意如果这里用的是普通的maven-jar-plugin生成的可执行jar大概率是有问题的。因为普通jar插件不会把你项目里的Spring Boot依赖一起打进去就算设置了Main-Class运行时也会因为找不到Spring相关类而继续报错。正确做法是使用Spring Boot官方提供的spring-boot-maven-plugin它在打包阶段做了两件最关键的事把项目所有依赖repackage到jar内部的BOOT-INF/lib目录下以及在MANIFEST.MF里生成正确的Main-Class和Start-Class属性。前者负责让fat jar能独立运行后者负责告诉JVM“启动入口是org.springframework.boot.loader.JarLauncher然后调用你的启动类”。3.2 第二步把你自己的启动类指给插件有些项目在pom.xml里已经引入了Spring Boot插件但还是报“主清单属性缺失”这时候就要检查是不是插件不知道你的启动类在哪里。大部分情况下Spring Boot插件会自动扫描项目里带SpringBootApplication注解的类并把它作为启动类。但如果你的项目结构比较特殊比如有多个main方法、或者启动类写在了非标准目录下插件可能会找错甚至找不到。保险的做法是显式指定plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.example.YourApplication/mainClass /configuration /plugin这里把com.example.YourApplication替换成你自己的启动类路径。3.3 第三步在IDEA里怎样正确配置打包再回到IDEA操作层面。很多人习惯在IDEA右侧Maven面板里双击package命令进行打包这个方式其实是最靠谱的因为它严格走了Maven生命周期。但也有特殊情况。有些老项目或特殊场景下你会用IDEA的File - Project Structure - Artifacts来手动配置打包。这时候就需要特别注意IDEA手动Artifact打包默认不会生成Main-Class属性。如果你确实想用IDEA的Artifact方式打包需要在Artifacts配置界面里手动把启动类加进去具体操作步骤我放在下面打开File - Project Structure - Artifacts选中你的打包配置点击右侧的按钮选择Main Class在弹出的窗口里选中你的启动类点击OK重新打包这套操作做完之后IDEA会在生成jar包时在MANIFEST.MF里写入Main-Class。不过说实话我对IDEA手动Artifact方式并不推荐。原因很简单这种方式不会像Maven那样自动处理依赖如果你的项目引入了第三方jar包运行时大概率会遇到ClassNotFoundException。与其花时间配置Artifacts不如回归Maven的标准打包方案。如果你赶时间我的建议是先检查pom.xml有没有spring-boot-maven-plugin加上之后再执行mvn clean package。这个操作能解决Spring Boot项目里80%以上的同类问题。4. 非Spring Boot项目的替代方案4.1 方案一用javac和jar命令手动打包如果你不是Spring Boot项目就是一个普通的Java程序或者项目里只有一两个文件那完全没必要引入Maven那么重的构建工具。用JDK自带的工具就能搞定。先写一个简单的启动类// com/example/Main.java package com.example; public class Main { public static void main(String[] args) { System.out.println(Hello, jar!); } }然后按下面三步操作第一步编译Java文件javac com/example/Main.java如果你的环境是中文Windows系统这里最好带上编码参数不然中文注释或字符串很可能报乱码错误javac -encoding UTF-8 com/example/Main.java第二步编写MANIFEST.MF文件。在项目根目录新建一个文件名字自定这里以MANIFEST.MF为例Manifest-Version: 1.0 Main-Class: com.example.Main文件最后要有一个空行这是MF文件的格式要求之前有不少人在这个细节上踩过坑——如果最后一行不是换行符MF文件的解析可能会出问题主类属性读不到。第三步用jar命令打包jar cfm myapp.jar MANIFEST.MF com/example/Main.class注意这一步的命令顺序是有讲究的c表示创建新jar包f表示指定jar包文件名m表示指定要合并的MANIFEST.MF文件。参数后面紧跟的是jar包的名字和MF文件名最后才是你要打包的class文件顺序千万别弄混。打包完成后验证一下是否成功java -jar myapp.jar控制台就能输出Hello, jar!了。4.2 方案二修改现有jar包的MANIFEST.MF还有一种常用场景你手上已经有一个打包好的jar里面class文件都是对的唯独MANIFEST.MF里没有Main-Class。这时候不需要重新编译直接修改jar包里的MF文件就行。操作思路是把jar包解压 - 修改MANIFEST.MF - 重新打包。先解压mkdir temp cd temp jar xf ../yourapp.jar然后编辑META-INF/MANIFEST.MF加上Main-Class一行Manifest-Version: 1.0 Main-Class: com.example.Main保存之后重新打包jar cfm yourapp-new.jar META-INF/MANIFEST.MF .注意jar命令的-C参数可以用指定目录但上面这种写法是在解压目录里直接执行的.表示把当前目录所有内容打进jar。如果执行时目录不对打包出来的jar层级会乱运行时会报ClassNotFoundException。顺便提一个热词里被提到的需求“jar包缝合”。如果你想合并两个jar包比如把依赖的lib包和业务代码缝在一起可以先解压把两者的内容放到同一个目录然后用jar cfm重新打包。但这种方式在依赖复杂时不推荐会碰到签名文件冲突、重复类覆盖等问题不如用Maven的shade插件或Spring Boot的repackage机制来得干净。4.3 有第三方依赖怎么办如果你的程序用了第三方库比如MySQL的JDBC驱动、Apache Commons之类的光把class文件打包是不够的运行时会报ClassNotFoundException。这时候有几种处理方式最简单粗暴的用-cp参数指定依赖目录运行时不依赖jar包里的lib目录。比如你有个lib/目录放着所有依赖java -cp yourapp.jar:lib/* com.example.MainWindows系统下把路径分隔符换成;java -cp yourapp.jar;lib/* com.example.Main注意这种方式下jar包里有没有Main-Class都不重要了因为你已经用-cp手动指定了入口类。如果你强迫症非得要java -jar一键启动那就得把所有依赖都塞进同一个jar里可以借助Maven的maven-assembly-plugin或maven-shade-plugin。shade插件相对友好一些它会自动处理依赖合并和签名冲突配置如下plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.2.4/version executions execution phasepackage/phase goals goalshade/goal /goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.example.Main/mainClass /transformer /transformers /configuration /execution /executions /plugin这样执行mvn package之后生成的jar包就能直接用java -jar运行了依赖已经全部被塞进了同一个包里。5. 特殊场景与Kotlin/JVM其他语言的注意事项5.1 引入外部jar包后报主清单缺失有些项目的报错时间和“主清单属性”并没有直接关系但会让你误以为是这个错误。比如你在IDEA里通过Project Structure - Modules - Dependencies - 手动引入了外部的jar包比如阿里的FastJSON、本地的JDBC驱动编译没问题打包后运行也报同一个错。这种情况的原因依然是IDEA手动引入的外部jar不会自动打包进最终的可执行jar里。运行时类加载器找不到这些类但奇怪的是JVM抛出的错误信息通常是ClassNotFoundException而不会停留在“主清单属性”这一步。这时候如果报的是主清单属性十有八九是入口类压根没找到。解决方式有两种一是用Maven把外部jar安装到本地仓库然后在pom.xml里正常引用二是用上一节提到的shade插件把外部jar的所有类重新定位后合并。5.2 Fat jar从外部运行时提示没有主清单属性这个坑非常隐蔽。之前帮人排查过一个问题Spring Boot项目用IDEA直接运行没问题mvn package打包也成功但把打出来的jar包拷到另一个目录或服务器上执行就报“没有主清单属性”。后来发现根因是IDEA在命令行执行mvn package时调用的Maven和IDEA内置配置的Maven不是同一个版本某些老版本Maven对jar插件的配置解析有问题导致MANIFEST.MF内容不完整。排查方式不复杂用解压工具看看打出来的jar包里的META-INF/MANIFEST.MF内容长这样就没问题Manifest-Version: 1.0 Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: com.example.YourApplication Spring-Boot-Version: 2.7.18如果看到没有Start-Class那你的jar插件配置确实有鬼请检查IDEA里Settings - Build Tools - Maven的配置换成你命令行里用的那个Maven路径再重新打包。5.3 如果项目里有多个main方法有些项目会在测试代码或工具类里写多个main方法打包时插件可能会“选错”启动类。Spring Boot插件的规则是优先找带SpringBootApplication注解的类如果找不到就会报错或者指到一个随机的类上。这种情况可以像下面这样在pom.xml里指定给Spring Boot插件显式配置mainClass同时给maven-jar-plugin配置archive - manifest - mainClass双保险。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId configuration archive manifest mainClasscom.example.YourApplication/mainClass /manifest /archive /configuration /plugin5.4 Kotlin项目顺便提一句如果你用Kotlin写JVM程序入口类通常是com.example.MainKt文件名加Kt后缀。打包时要格外注意Main-Class填的不是类名而是Kotlin编译器生成的那个Kt类。比如文件叫App.kt里面写了fun main(args: ArrayString) { ... }那么Main-Class对应的值应该是AppKt而不是App。很多人在Kotlin项目里踩到同样的报错就是因为忽略了这一点。6. 实用排查清单从报错到定位的完整路径这里给你整理一份可以直接照做的排查清单按顺序跑一遍基本能定位99%的问题。排查步骤操作方式判断标准1. 查看jar包内部结构用解压软件打开或jar tf xxx.jar是否存在META-INF/MANIFEST.MF2. 查看MANIFEST.MF内容unzip -p xxx.jar META-INF/MANIFEST.MF是否有Main-ClassSpring Boot项目是否还有Start-Class3. 确认启动类是否存在在jar里找一下启动类对应的class路径jar tf xxx.jar | grep YourMainclass文件必须在且路径和Main-Class值的点号对应4. 确认打包插件检查pom.xml的build节点Spring Boot项目必须有spring-boot-maven-plugin5. 检查启动类签名用javap或反编译工具main方法签名必须是public static void main(String[])这里再透露一个小技巧Windows环境下如果不想装解压软件也可以在命令行里用JDK自带工具直接查看MF文件内容jar xf yourapp.jar META-INF/MANIFEST.MF type META-INF\MANIFEST.MF命令跑完记得把这个目录清理掉不然容易弄乱项目结构。7. 关于反编译和入口定位的一些经验另外一个相关的热词是“反编译jar”。其实很多时候手里拿到的jar包没有源码又不知道它的入口类是谁这时候确定“主清单属性”里应该写什么就变得很关键。JVM提供了java -jar之外另一种启动jar的方式可以不依赖MANIFEST.MF的Main-Classjava -cp yourapp.jar com.example.Main这种方式需要你自己知道入口类属于手动指定。那怎么知道入口类是谁有几个办法最有效的如果你有源码直接找带main方法的类就行。没有源码的先把jar反编译了找。常用的反编译工具有JD-GUI老牌图形化反编译工具拖入jar包就能看源码适合快速浏览。CFR命令行反编译工具适合批量处理和集成到脚本。IntelliJ IDEA自带的反编译器在IDEA里打开jar依赖时直接点进去就能看反编译后的源码方便而且无需额外工具。通过查看源码里哪个类有public static void main(String[] args)然后反推它的全限定名就能写进MANIFEST.MF了。另外还有一个小众但极度隐藏的办法用Spring Boot打包的fat jar入口通常非常统一直接指定org.springframework.boot.loader.JarLauncher旧版本或org.springframework.boot.loader.launch.JarLauncher新版本就能把jar当普通jar启动。这种方式的作用在于当一个fat jar的MANIFEST.MF损坏时你可以用这条命令绕过它直接执行内部的Spring Boot应用方便临时恢复服务或排查问题。8. 写在最后的实操心得这个报错信息虽然短但背后牵扯的知识点其实不少——jar包的内部结构、Maven插件机制、JVM类加载机制、Spring Boot的启动方式全揉在了一起。我个人在实际排查中最大的体会是遇到这个报错先别急着改代码先看MANIFEST.MF里的实际内容。这一步成本最低、信息量最大。很多人一上来就搜“怎么加主清单属性”结果照着网上的命令一顿操作把原本没问题的项目结构搞乱了。还有一个小建议强烈建议所有打包环节都走Maven/Gradle的重复打包流程不要手动解压再打包特别是在Windows环境下。手动重新打包很容易出现换行符问题导致MANIFEST.MF的Main-Class行被错误合并反而引出一堆新问题。最后分享一个更省心的经验如果你用的是Spring Boot 2.3以上版本Maven打包时自带spring-boot-maven-plugin会替换原来的jar生成可执行jar和原始jar两个文件其中原始jar以.original结尾。如果你把.original后缀误改回.jar拿去执行也会报“没有主清单属性”因为原始jar根本没做repackage。收到.original文件的时候记住它不是用来直接启动的而是给其他项目做依赖用的。这大概就是我踩过这个坑之后攒下的全部经验了。按清单排查一遍大多数主清单属性的问题都能在两三分钟内解决希望对你也有帮助。