在控制台敲下java -jar xxx.jar等了两三秒屏幕上弹出来一行英文提示错误: 主类 xxx.jar 中没有主清单属性或者完整版是错误: 找不到或无法加载主类 xxx.jar Caused by: java.lang.ClassNotFoundException: xxx.jar很多人的第一反应是“我是不是没装Java”“是不是JDK版本不对”。其实都不是。这个报错的英文原话是no main manifest attribute in xxx.jar翻译过来就是“这个jar包里没有主清单属性”。这里的“主清单属性”指的是META-INF/MANIFEST.MF文件里的Main-Class配置项。Java靠它来知道“你让我用java -jar启动我应该去执行哪个类的main方法”。我自己第一次遇到这个报错是在工作第二年当时用IDEA的Artifacts方式导出一个jar包扔到服务器上执行结果就是这一句。当时没有搜索引擎能一秒钟给答案硬是把jar包解压开来一行一行看清单文件才搞明白发生了什么。现在这个报错依旧高频出现尤其是刚接触Spring Boot、Maven多模块项目的同学几乎每隔几天就会有人来问。这篇博文就把这个报错彻底拆开从原因、排查到解决方案全部捋一遍覆盖纯命令行打包、Maven打包、Spring Boot打包、fat jar打依赖、以及反编译jar包排查入口类这些常见场景。1. 报错的真相主清单属性是怎么丢掉的1.1 先说清楚“主清单属性”到底是什么MANIFEST.MF是JAR包里的一个描述文件路径固定为META-INF/MANIFEST.MF。它本质上是一个键值对文本最简单的长这样Manifest-Version: 1.0 Created-By: 1.8.0_292 (Oracle Corporation) Main-Class: com.example.demo.DemoApplication这里最关键的就是Main-Class这一行。它告诉JDK当用户执行java -jar xxx.jar时应该加载com.example.demo.DemoApplication这个类并调用里面的public static void main(String[] args)。注意Main-Class必须是带完整包名的类名不能写DemoApplication也不能写DemoApplication.class。你要是写错了报错就变成了“找不到或无法加载主类”但往往也容易和“没有主清单属性”混在一起。另外还有一个隐藏的细节很多人不知道清单文件末尾必须保留一个换行符。也就是说最后一行Main-Class写完以后文件结尾要有一个空行否则某些JDK版本在解析时可能会认为这个属性无效。这个坑在手动编写manifest文件时特别容易踩。1.2 为什么报错信息里只给了这么含糊的一句话java -jar的执行机制决定了它不会像IDE启动项目那样去扫描所有class找main方法。Java的启动器只会读取jar包内的MANIFEST.MF如果里面没有Main-Class键就直接给你上面那句no main manifest attribute。它不会告诉你“你的jar包里有哪些类”也不会建议你“是不是该把入口类写上”。信息量极少所以新手很容易懵。更让人无奈的是用IDE的“导出jar包”功能时默认选项是“保存到JAR文件中”它并不会自动帮你生成Main-Class除非你手动在导出向导里选择“从具有main方法的类启动”并指定入口类。很多人根本不知道有这一步于是打出来的jar包自然就没有主清单属性。同样的情况也常见于Maven项目如果你在pom.xml里只配置了maven-jar-plugin而没指定mainClass打出来的jar包同样没有Main-Class。对Maven的maven-jar-plugin默认不会自动探测你的main类。它只是一个“打普通jar包”的工具不像Spring Boot插件那样会自动去配置。1.3 症状识别出现这个报错通常你已经在这三种场景里根据我这些年的经验遇到这个报错基本逃不出下面这三种场景场景打jar包方式报错特征IDE导出常见的普通JARIDEA/Eclipse的Artifactsno main manifest attributejar包体积很小Maven project 未配置插件mvn clean package用默认maven-jar-plugin包能打出来但启动即报错Spring Boot项目 把spring-boot-maven-plugin漏了mvn clean package但只用了spring-boot-starter-parent包能打出来但同样报这个错区分这三种场景的意义在于解决方案不一样。如果只是给普通jar包补一个Main-Class那很简单但如果是Spring Boot项目你除了要补Main-Class以外还得确保把依赖jar包一起打进去否则运行时会报ClassNotFoundException: org.springframework.boot.SpringApplication。这就涉及“fat jar”的概念后面会专门讲。2. 动手排查先把jar包“解剖”开看看到底缺了什么在动手改构建配置之前我强烈建议你先做一次手动排查。这花不了两分钟却能让你彻底看清问题根源。我一直觉得排查这类构建期问题最好的方式就是“别把它当成黑盒”。2.1 第一板斧查看JAR包内容列表用下面的命令列出jar包里的所有条目jar tf xxx.jarjar tf是JDK自带的参数tf表示list table of contents。如果你更习惯用解压工具也可以把jar包后缀改成zip直接解压。看的时候重点看有没有META-INF/MANIFEST.MF文件夹。如果这个文件压根不存在那已经不用继续查了问题就是清单文件丢失。如果存在再看第二板斧。2.2 第二板斧直接查看MANIFEST.MF内容在Linux/Mac上可以直接用unzip命令抽取单文件到标准输出unzip -p xxx.jar META-INF/MANIFEST.MF在Windows上你可以用jar xf xxx.jar META-INF/MANIFEST.MF把它解压到当前目录然后打开看。或者直接扔进解压软件双击META-INF文件夹里的MANIFEST.MF。重点看有没有这一行Main-Class: com.example.Main如果这个键不存在或者写成了Main-Class: Main没有包名那就找到了报错根因。2.3 第三板斧确认入口类是否真的存在有一种情况容易被忽略manifest文件里写了Main-Class但类不存在。这种情况下报错通常不是“没有主清单属性”而是找不到或无法加载主类。但如果你通过反射调用、或者用了某些打包插件报错也有可能在“主清单属性”这个阶段就炸掉。验证方式是用jar tf搜索对应class文件jar tf xxx.jar | grep com/example/Main.class注意在jar包中类的路径是包名转为目录结构后的路径也就是com/example/Main.class而不是com.example.Main。这一步能同时验证一个隐藏问题你是不是把.class文件打错目录了比如直接丢在jar包根路径下没有按包路径放。如果class文件路径不对就算Main-Class写对了也没有用。2.4 顺手确认class版本与JDK匹配如果你的jar包是从别的机器、别的同事手里拿过来的还有一个小概率原因是class 文件版本号和本机JDK不匹配。你可以在jar包里取出一个class文件然后用javap查看它的版本javap -verbose com/example/Main.class | grep major看到的major version如果是61代表编译用的是Java 17而你本机是Java 8那运行时会报UnsupportedClassVersionError。这个报错虽然不会直接变成“没有主清单属性”但如果你在用IDE运行某些定制化启动器时它会非常具有迷惑性所以我把它放在排查清单里。这套“解剖”流程走完你基本就能确定问题出在哪个环节接下来再去修构建配置就有的放矢了。3. Maven/Spring Boot项目打包的最佳修复姿势大多数Java项目现在都基于Maven构建所以这一节是重点。我见过太多人在这里把maven-jar-plugin、spring-boot-maven-plugin、maven-shade-plugin搞混导致越改越乱。3.1 别再把Spring Boot打包插件和普通打包插件混为一谈如果你用的是Spring Boot最简单的修复方式就是在pom.xml里加上spring-boot-maven-pluginbuild plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build然后重新执行mvn clean package这个插件在做repackage时会做两件普通插件做不到的事自动把Main-Class设置为项目的入口类也就是包含main方法的那个类把项目所有依赖jar包“压”进最终生成的jar包里同时额外生成一个org/springframework/boot/loader目录里面是Spring Boot自定义的类加载器用于在fat jar内部按顺序加载BOOT-INF/lib下的依赖。所以Spring Boot项目打出来的jar包会长这样xxx.jar ├── META-INF/MANIFEST.MF ├── org/springframework/boot/loader/... ├── BOOT-INF/classes/ │ └── com/example/... └── BOOT-INF/lib/ ├── spring-core-5.x.jar ├── spring-web-5.x.jar └── ...注意其中的Main-Class在MANIFEST.MF里并不是你的业务类而是Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: com.example.demo.DemoApplication这里容易让人困惑为什么Main-Class不是自己的类因为Spring Boot需要先启动它自己的类加载器再由这个类加载器去加载BOOT-INF/classes和BOOT-INF/lib下的类最后执行Start-Class里的入口方法。理解了这一点就不会再去手动乱改manifest了。3.2 非Spring Boot的普通Maven项目怎么指定主类如果你的项目不是Spring Boot只是普通Java应用那更合适的插件是maven-jar-plugin。配置方式如下plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.4.1/version configuration archive manifest mainClasscom.example.Main/mainClass /manifest /archive /configuration /plugin这样执行mvn clean package后打出来的jar包META-INF/MANIFEST.MF里就会带Main-Class: com.example.Main可以直接用java -jar xxx.jar启动。但这里有个后患如果项目依赖了第三方jar包那么普通jar包不会帮你把依赖打进去运行时大概率会报ClassNotFoundException。这时候如果你只是想把启动跑通可以临时用-cp指定依赖路径java -cp xxx.jar:lib/* com.example.Main注意这里的入口写法是完整类名不是jar包名。这其实就是热搜词里“java jar包指定函数入口”背后真正的问题用-jar走的是manifest配置用-cp走的是手动指定入口类。如果想不打fat jar但又想省事更推荐的方式是用maven-dependency-plugin在打包时把依赖复制到一个lib目录然后用脚本启动mvn dependency:copy-dependencies -DoutputDirectorylib这样java -cp target/xxx.jar:lib/* com.example.Main就能顺利跑起来。对于不想引入复杂插件、又需要分发到多台服务器的项目这种方式反而比打fat jar更省心因为依赖是外置的更新某一个依赖时不用重新打整个应用。3.3 打fat jar时manifest被插件覆盖的问题如果你用maven-shade-plugin来打fat jar那你要注意一个隐性问题maven-shade-plugin在合并多个jar包的META-INF/MANIFEST.MF时默认会保留主项目生成的manifest但如果你之前用maven-jar-plugin设置了Main-Class而shade插件自己没写manifestResourceTransformer有可能导致最终manifest里没有入口类。正确配置是这样的plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.5.2/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这里面的ManifestResourceTransformer不仅会设置Main-Class还会负责合并多个依赖jar包里的META-INF/services文件。如果你用shade打了fat jar后出现ServiceConfigurationError或者某些SPI特性失效多半就是缺少这个transformer。3.4 多模块项目里的入口类定位如果你在一个Maven多模块项目里遇到这个报错概率最高的情况是你在聚合模块父模块里直接执行了mvn clean package然后把父模块打出来的jar包拿去运行。父模块的packaging通常是pom要么打出来的是一个没什么内容的空壳要么依赖模块各自打了jar而父模块没有入口类。正确的做法是进入包含main方法的那个子模块目录在子模块里执行打包cd demo-web mvn clean package或者使用Maven的-pl参数指定模块并加上-am同时构建依赖模块mvn clean package -pl demo-web -am这里给一个很实用的建议不要把spring-boot-maven-plugin配置在父POM里让所有子模块继承。如果一个子模块不是Spring Boot应用它打出来的jar会莫名变大而且还会带一个根本没有意义的JarLauncher。最好只在真正需要打包可执行jar的模块上配置这个插件。4. 不依赖构建工具的“裸打”方案与命令不是所有项目都用Maven或Gradle。有时候你只是手头有一个.java文件或者从网上下了一个开源项目的源码想快速打一个能跑的jar包。这种场景下直接用命令行操作更痛快。4.1 先用javac把源码编译成class假设你有这样一个入口类package com.example; public class Main { public static void main(String[] args) { System.out.println(Hello, Jar!); } }先创建对应的目录结构并编译mkdir -p classes javac -d classes src/com/example/Main.java如果项目里还有其他类直接一块儿编译即可也可以用通配符javac -encoding UTF-8 -d classes $(find src -name *.java)4.2 生成带Main-Class的manifest文件方式这里有两种方式可以给命令行jar工具指定入口类都不需要手动编辑manifest文件jar --create --file app.jar --main-class com.example.Main -C classes .如果JDK版本较老Java 8时期的写法是jar cvfe app.jar com.example.Main -C classes .其中e代表entry point也就是设置Main-Class。注意这一招会直接在生成的manifest里写入入口类信息不需要你手动建MANIFEST.MF。如果你需要自定义manifest的其他属性比如Class-Path或Implementation-Version那就先生成一个文本文件Main-Class: com.example.Main然后带上它一起打包jar cvfm app.jar manifest.txt -C classes .这里f表示输出到文件m表示使用外部manifest文件。再次强调manifest内容末尾记得留一行空行否则可能出现解析异常。4.3 运行期还是要依赖一堆jar包怎么办如果main方法里用到了第三方库比如用了Google的Gson那么光把Main.class和Gson.jar塞进同一个目录还不够java -jar app.jar启动时默认不会读到旁边的依赖jar包。简单办法是继续用-cp方式运行java -cp app.jar:lib/gson-2.10.1.jar com.example.MainWindows下的分隔符是分号java -cp app.jar;lib\gson-2.10.1.jar com.example.Main更省事的办法是在manifest里写Class-PathMain-Class: com.example.Main Class-Path: lib/gson-2.10.1.jar lib/commons-lang3-3.12.0.jar这样在jar包旁边建一个lib目录把依赖jar放进去然后直接执行java -jar app.jar就能自动找到。Class-Path的路径是相对于jar包所在目录的。5. 从热搜词延伸的几个常见坑这个话题之所以能衍生出那么多关联搜索词说明大家在实际操作中踩的坑不止一个。我挑几个最典型的展开讲每一个都是我亲眼见过、或者亲手帮别人排查过的。5.1 “jar包怎么缝合”与依赖合并的那些事有人把多个jar包合并成一个过程叫做“缝合”。手动“缝合”的方式很简单比如cd jar1 jar xf ../jar1.jar cd ../jar2 jar xf ../jar2.jar cd .. jar cf merged.jar -C jar1 . -C jar2 .但你很快就会遇到两个问题同名文件会互相覆盖尤其是META-INF/MANIFEST.MF、META-INF/services以及一些xxx.properties配置其次如果两个jar包里都提供了同一个类的不同版本最终谁排在前谁生效行为非常不确定。这就是为什么我建议你用maven-shade-plugin或Spring Boot插件而不是手动解压合并。真正要做可靠的依赖合并需要考虑SPI文件的追加合并这也是shade插件里ServicesResourceTransformer存在的原因。5.2 从网上下载的第三方jar包运行时也要注意入口有些场景是你从官网下载了一个JDBC驱动或者一个SDK的jar包想直接用java -jar xxx.jar跑起来测试结果就遇到了“没有主清单属性”。比如下载了MySQL的JDBC驱动mysql-connector-j-8.0.33.jar然后执行java -jar mysql-connector-j-8.0.33.jar这时候报“没有主清单属性”是完全正常的。JDBC驱动本来就是给别的Java程序调用的库它并不需要提供main入口。如果你非要测试驱动能否正常加载正确的做法是写一个小测试类或者直接用java -cp玩一下java -cp mysql-connector-j-8.0.33.jar com.mysql.cj.jdbc.Driver这个类虽然没有main方法但至少会走一遍类加载如果类路径正确就不会报ClassNotFound。类似的还有TDengine的JDBC驱动jar包以及各类国产数据库如神通的JDBC驱动。看到“没有主清单属性”时先判断一下这个jar包到底是“可执行应用”还是“被依赖的库”不要盲目去改它的manifest。5.3 反编译jar包来查找Main-Class和调用链当手里只有一个编译好的jar包跑去问别人“这个jar的入口类是什么”最直接的思路就是反编译。常见工具有JD-GUI、CFR、Fernflower。IDEA自带的反编译器就很够用直接把jar包拖进IDEA展开META-INF/MANIFEST.MF就能看到Main-Class。如果你更习惯命令行也可以用javap来“反编译”类签名。比如先找到入口类然后查看它有没有main方法javap -classpath app.jar com.example.Main输出如果包含public static void main(java.lang.String[]);就说明这个类可以作为入口。我之前排查过一个老项目的调用链问题就是先解压jar包找到manifest的Main-Class再顺着main方法一层层看它调用了哪些类最后才定位到一个配置类加载失败的问题。所以“jar包查找调用链”这个搜索词的背后就是这套反编译加源码定位的思路。5.4 IDE里右键运行没问题命令行一跑就报错怎么回事这是搜索里很典型的一种场景在IDEA里点绿色三角形运行得好好的到命令行执行java -jar就报“没有主清单属性”。原因很简单IDEA运行项目时用的是它自己计算出的classpath它知道你项目里的out/production目录在哪、所有依赖jar包在哪然后直接调java命令运行入口类。它根本不需要读取jar包里的MANIFEST.MF。而java -jar强制走jar包内的manifest所以两边行为完全不同。解决办法就是前面说的要么用正确的Maven插件重新打包要么在IDEA里把“构建工件”方式改对。个人建议能走Maven的就别用IDE的ArtifactsArtifacts是IDE自己维护的一套构建逻辑换了电脑、换了IDE版本很容易踩坑。6. 最终自查清单与运维兜底经验我把排查这个报错的完整路径整理成一张清单你以后可以直接对照着查检查项执行命令/操作说明确认jar包与JDK环境java -versionfile xxx.jar排除基本工具问题查看jar包结构jar tf xxx.jar检查是否存在META-INF/MANIFEST.MF查看manifest内容unzip -p xxx.jar META-INF/MANIFEST.MF检查Main-Class是否存在确认入口类是否存在于jar包jar tf xxx.jar | grep com/example/Main.class包路径是否正确Maven项目修正入口配置maven-jar-plugin的mainClass普通Java应用方案Spring Boot项目修正配置spring-boot-maven-plugin同时解决依赖打包fat jar需求使用maven-shade-plugin配合ManifestResourceTransformer多依赖项目使用运行期依赖缺失java -cp xxx.jar:lib/* com.example.Main临时启动方案服务化部署使用Systemd/nssm包装启动命令生产环境更稳妥最后说一点运维层面的经验。当你解压一个jar包改manifest再重新打包时有一个非常容易出错的动作解压后再jar打包时META-INF目录里的文件权限、编码格式都可能发生变化。尤其是manifest文件如果被Windows记事本打开过可能会被存成带BOM的UTF-8格式JDK解析时会因为BOM字符把属性值读错出现“明明写了Main-Class却依然说没有主清单属性”的诡异现象。所以我一般建议能不动jar内部文件就不动。真要改manifest直接在源码工程的构建配置里改改完重新打包这比解压再打包干净得多。如果必须用解压修改的方式请在修改后用unzip -p检查文件二进制内容确认开头没有EF BB BFUTF-8 BOM这类额外字节。另外如果你经常要给不同机器分发Java应用我的个人习惯是构建阶段用Maven或Gradle把工程打成标准可执行jar再把启动脚本写好不要总手动敲长串的java -cp。启动脚本的好处是能把JAVA_HOME、CLASSPATH、JVM参数固定下来避免换台机器、换个用户就出现类路径不对或者入口类找不到的问题。这个报错本身并不难难的是在报错出现时能快速判断“我到底该改哪里”。现在你手里有了从原理到排查再到修复的完整链路下次再看到“jar中没有主清单属性”大概率是两分钟之内就能定位解决的事。