先别急着怀疑人生,这个报错在Java开发路上基本人人都会撞上一次——在控制台敲下java -jar xxx.jar想启动程序,结果终端无情地回了一句no main manifest attribute, in xxx.jar,中文环境则显示“jar中没有主清单属性”。我第一次遇到时真以为JAR包组装坏了,后来才明白问题出在入口信息上。这篇就把这事的来龙去脉、最快应急手段、以及一劳永逸的打法彻底讲清楚,适合刚入门的Java新手,也适合已经遇到过、但没弄明白原理的同学参考。
1. 这个报错到底在说什么:主清单属性是什么
1.1 一次完整的java -jar执行流程
要理解这个报错,先得知道java -jar启动一个JAR包时发生了什么。JAR包本质上就是ZIP压缩包,里面装着编译好的.class文件和其他资源文件。但在JAR包的固定位置META-INF/MANIFEST.MF里,有一个清单文件,里面可以写各种元信息。
你执行java -jar xxx.jar的时候,JVM会先把这个JAR包打开,找到META-INF/MANIFEST.MF,然后从里面查找一个叫Main-Class的属性。这个属性写的是入口类的完整类名,比如com.example.MyApplication。JVM拿到这个类名后,才会去加载这个类,然后调用它的public static void main(String[] args)方法。
如果清单文件里没有Main-Class,或者这个属性当前为空,就会直接抛出no main manifest attribute,程序压根不会运行。报错信息里还会把JAR包的文件名一起列出来,算是帮你定位了是哪个包出了问题。
1.2 正常JAR包与可执行JAR包的区别
很多人被这个报错坑到,是因为把“普通JAR包”和“可执行JAR包”搞混了。Maven或Gradle打包出来的JAR,默认只是“库文件”,也就是给别人项目引用的工具包,它们根本不需要入口。比如你日常引入的commons-lang3.jar、mysql-connector-java.jar,清单文件里一般只有基本描述信息,没有Main-Class,也没必要有。
可执行JAR包则是在清单里明确写了入口类的包,用java -jar能直接启动。打个比方,普通JAR是“零件箱”,可执行JAR是“整机”。你把零件箱插上电想开机,当然没反应——里面根本没有“启动按钮”对应的那个类。
所以遇到这个错误,第一反应不是去找什么灵丹妙药,而是判断:你手上这个JAR到底是不是设计成可执行的。如果不是,要么换一条运行方式,要么重新打包成可执行JAR。
1.3 触发这个报错的五种典型场景
结合我在控制台摸爬滚打的经验,这个报错通常出在下面几种情况里:
- 直接拿项目依赖的普通库JAR包用
java -jar运行,这最常见,新手尤其容易犯。 - Maven项目打包时没有配置
maven-jar-plugin,或者配置文件里漏写了主类,打出来的包自然没有入口。 - Spring Boot项目没有引入
spring-boot-maven-plugin,导致Maven默认打的只是个普通JAR。 - 从网上反编译别人的JAR,改完代码后重新打包,忘了写回
Main-Class。 - 把多个JAR包“缝合”到一个包里时,解压覆盖过程中把
META-INF/MANIFEST.MF弄坏或覆盖成空文件。
后面几种场景在下面几个章节都会给到具体解法。先讲应急方案,让你先把程序跑起来。
2. 最快速的应急方案:不重新打包也能跑起来
2.1 用java -cp直接指定主类
如果手头只有一个JAR包,又不想重新走一遍构建流程,最快的办法就是绕过java -jar,改用java -cp指定入口类。命令长这样:
java -cp yourjavajar.jar com.example.MainClass这里-cp(即-classpath)指定JAR包路径,后面直接跟上入口类的全限定名。这种方式完全不需要读清单文件,你只是告诉JVM“这个类的main方法就是入口”,于是能正常启动。
前提是,你得先知道入口类到底叫什么。实在不知道的话,可以用一条命令把JAR包里的class列表翻出来看个大概:
jar tf yourjavajar.jar如果输出里能看到类似com/example/App.class,那入口类多半就是com.example.App。如果App.class没有挂在com.example包下面,而是直接躺在根目录,那入口类就是App。当然这只是猜入口,更靠谱的是解压看清单和反编译确认,这在后面章节细说。
2.2 如何准确获取正确的入口类名
有些JAR包的清单文件其实有Main-Class,只是你运行的方式不对。比如清单里写了入口,但你硬要用java -cp去加载别的类,也会报Could not find or load main class。遇到这种混淆情况,建议先直接看清单文件内容:
unzip -p yourjavajar.jar META-INF/MANIFEST.MF用unzip -p的好处是只把指定文件内容打印到控制台,不用把整个JAR包解压出来。你会看到类似输出:
Manifest-Version: 1.0 Created-By: Maven Jar Plugin 3.3.0 Built-By: user Main-Class: com.example.Launcher如果看到了Main-Class,直接复制后面的类名去执行:
java -cp yourjavajar.jar com.example.Launcher如果清单文件里压根没有Main-Class,那说明这个包设计之初就不是可执行的。这时候你得从代码层面确认入口,看项目源码里哪个类带main方法,或者用反编译工具打开反编译看看。
2.3 带依赖JAR运行的Class-Path处理
用-cp运行的一个常见坑是:你的主类依赖了第三方库,光把主程序JAR放在classpath里还不够,一运行就可能报ClassNotFoundException或NoClassDefFoundError。这种情况要手动把依赖JAR也加进classpath,命令变成:
java -cp "yourjavajar.jar:lib/*" com.example.MainClassLinux和macOS环境下多个路径用冒号:分隔,Windows下用分号;。lib/*表示把lib目录下的所有JAR都加进去,注意星号只匹配JAR包,不会递归子目录,也别写成lib/*.jar,那种写法在某些JDK版本上反而可能不生效。
如果你的系统里装了多个版本的JDK,建议先看一眼当前默认版本:
java -version不少时候程序跑不起来不是入口类问题,而是JDK版本和编译级别不匹配,这个在排错时也要纳入考虑。
3. 治本方案:重新打包让主清单属性完整
应急方案只能临时顶着,要想java -jar能直接跑,还得从构建配置上下手。按你的项目用的构建工具分三种情况来解决。
3.1 Maven项目的一次性修复
Maven项目里,控制JAR包清单的是maven-jar-plugin。如果构建配置里没写主类,打出来的包就没有Main-Class。在pom.xml的<build><plugins>里加上这一段配置:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <mainClass>com.example.MainClass</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build>重新执行mvn clean package,再用unzip -p看清单,Main-Class就会出现。这种配置适合简单项目,主类没有依赖第三方库的情况。
如果你的程序依赖了很多第三方库,直接java -jar一样会报ClassNotFoundException,因为JVM找不到依赖。两种常见解法:一是把Class-Path也写进清单,但这种做法要在配置里维护相对路径,很麻烦;二是用maven-shade-plugin把依赖全打到一个“胖JAR”里,生成的包体积大,但用起来省心。shade插件配置如下:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.MainClass</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin>这里关键是用ManifestResourceTransformer来把主类写进合并后的清单,否则shade出来的包同样会丢入口。
3.2 Spring Boot项目的特殊处理
Spring Boot项目如果用了spring-boot-starter-parent作为父工程,打包时默认带spring-boot-maven-plugin,所以mvn package直接能打出可执行JAR。但如果你在<plugins>里手动覆盖了打包插件,却不小心漏掉了它,就会打出普通JAR,启动必然报“jar中没有主清单属性”。
Spring Boot的正确打包配置是这样:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>如果项目没有继承spring-boot-starter-parent,需要额外指定执行目标:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>repackage这个goal会把你原来的普通JAR再处理一遍,把三方依赖塞进去,并在清单里写入真正的启动类。注意观察打包输出,Spring Boot生成的original JAR和最终可执行JAR一般是两个文件,别拿错。
3.3 Gradle项目的配置方式
Gradle项目如果没做额外配置,默认的jar任务打出来的也是没有入口类的普通包。最简单的修复是在build.gradle里给jar任务加上manifest配置:
jar { manifest { attributes 'Main-Class': 'com.example.MainClass' } }同样地,如果依赖复杂,建议用application插件配合installDist,或者用 Shadow 插件生成胖JAR。用Shadow的话配置是:
plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' } shadowJar { manifest { attributes 'Main-Class': 'com.example.MainClass' } }执行./gradlew shadowJar,得到的xxx-all.jar就能直接java -jar运行。Spring Boot的Gradle项目更简单,加上org.springframework.boot插件后,运行bootJar任务即可生成可执行JAR,这块和Maven的repackage逻辑类似。
3.4 手动修改已有JAR包清单(zip/解压/重新打包)
有些情况下你没有源码,只有别人给的JAR,而且它确实可以修改,那就直接在二进制层面把清单补上。这种操作我做过很多次,思路就是三步:解压 → 改清单 → 重新打包。
第一步,解压JAR到临时目录:
unzip -d tempdir yourjavajar.jar第二步,编辑tempdir/META-INF/MANIFEST.MF,确保里面有这一行,并且入口类路径写对:
Main-Class: com.example.MainClass强烈建议用支持UTF-8的文本编辑器,别用系统自带记事本瞎改,后面我会讲编码坑。
第三步,重新打成JAR包,这里要注意命令参数顺序。最容易踩坑的是jar cfm的顺序,它和jar cfe不一样:
jar cfm new.jar tempdir/META-INF/MANIFEST.MF -C tempdir .-C tempdir表示进入tempdir目录后处理剩余文件,末尾的.表示把当前目录下所有文件打进包。如果输出的JAR包缺了资源文件,多半是这一步没转对目录。不打-C的话,打出来的JAR内部会多一层tempdir前缀,运行起来各种资源路径错乱。
如果你不想用命令行,也可以用7-Zip直接打开JAR包(它本质是ZIP),找到META-INF/MANIFEST.MF文件,拖出来改好再放回去。7-Zip会重新压缩,这招在Windows上可以避免装一堆命令行工具。
4. 特殊场景:反编译、合并JAR包后的清单修复
4.1 反编译后重新打包为什么容易丢主类
反编译JAR再重新打包的场景在外包和二次开发里很常见。有人用如JD-GUI、CFR反编译拿到.java文件,改完代码后用javac重新编译,再手动jar cf打出一个新包。打完之后java -jar直接报错,原因就是反编译的时候只关注了.class文件,压根没人去管MANIFEST.MF。
反编译后重新打包的正确做法是:打包前先把原JAR的META-INF目录完整保留下来,手工编译完.class文件后,使用包含原来清单文件的方式重新打包。命令顺序再强调一遍:
jar cfm new.jar META-INF/MANIFEST.MF -C ./classes .如果原来的清单里还有签名信息,比如META-INF/*.SF、*.RSA这些文件,直接沿用会引发SecurityException。这种场景建议把META-INF目录下除MANIFEST.MF之外的签名文件全删掉,因为改过代码后签名本来就是失效的。
4.2 多个JAR包合并时的Main-Class保留技巧
“JAR包怎么缝合”是社区里常被翻出来的问题,很多人试图把多个JAR包手动合成一个,直接用解压工具把A包的class扔进B包,或者用jar uf更新进去。这种方式最容易踩的坑就是:覆盖了对方的META-INF/MANIFEST.MF,或者合并之后入口类丢了。
我更推荐用构建工具来做JAR合并,而不是手工复制。Maven用shade,Gradle用shadow,它们都支持多个JAR的META-INF合并,还能让不同JAR的同名配置文件做对应处理。比如shade的transformers里可以配ServicesResourceTransformer来处理服务接口文件,单纯手工合并基本处理不了这种冲突。
如果你实在不方便用构建工具,坚持手工缝合,那请注意:合并前先把第一个JAR的META-INF/MANIFEST.MF复制出来,最后用这个清单重新打包,别指望解压工具自动帮你合并清单。还要留意不同JAR里如果有同名class,后面的会覆盖前面的,可能会导致运行时各种诡异错误。
5. 常见问题与排查技巧实录
5.1 清单文件格式的暗坑:换行、编码、签名
很多人改完清单文件,重新打包后依然报错,那我建议你按下面几个隐藏点逐一排查。
第一,Main-Class这一行必须是冒号+空格+类名的格式。写成Main-Class:com.example.Main(少了空格)、写错大小写、类名多个空格,都有可能解析失败。
第二,清单文件最后一个属性后面必须要有换行符。这是JAR规范要求,很多编辑器保存时默认不在文件末尾加换行,导致最后一行属性读取失败。这个问题非常隐蔽,我之前手动改清单时遇到过两次,花了很长时间才定位到。
第三,文件名别写错。明明写了Main-Class,却写在了META-INF/mf这种不存在的路径下,或者文件名大小写对不上。Linux服务器上是区分大小写的,MANIFEST.MF和manifest.mf是两个文件。
第四,如果原JAR带签名,你重新打包后又想保留签名,必须重新签名,否则会启动失败。签名文件只是覆盖了旧的,实际哈希早已对不上,删掉最省心。
为了验证你的清单是否写对,看一遍输出内容是最快的:
unzip -p new.jar META-INF/MANIFEST.MF确认最后一行的Main-Class正常,并且末尾确实有空行,再用java -jar试运行。
5.2 改完还是报错怎么办:先分清三类报错
有时候你已经把Main-Class写进清单了,控制台却还在报错。注意,这里要区分三个容易混淆的报错:
no main manifest attribute:清单没有主类属性,属于还没找到入口。Could not find or load main class:清单里写了入口,但JVM按这个类名去加载时没找到对应的class,说明类名写错、或class文件确实不在包里。ClassNotFoundException:主类找到了,但它依赖的某个第三方类在运行环境里不存在,这是依赖缺失问题。
第一类和第二类之间的转化特别常见,你要做的是回到上一步,用jar tf your.jar | grep 主类路径确认包内确实存在com/example/Main.class。如果包内没有,说明打包时漏了源文件,重新编译打包。如果第三个报错,优先检查当前classpath是否覆盖了所有依赖,或者直接改用shade打出胖JAR。
5.3 环境与命令的连带问题排查
除了JAR本身,环境层面的问题也可能让你误判成“主清单属性”问题。比如在Windows的PowerShell里执行多路径classpath时,路径分隔符要写成分号,很多人照抄Linux的冒号写法,结果报找不到类。要么换成;,要么在PowerShell里用数组形式,别硬记一个命令到处跑。
另一个高频问题是:控制台显示的中文报错和英文报错对不上。jar中没有主清单属性的英文原文常见是no main manifest attribute,这其实是同一个东西,不用怀疑是不同错误。
再提一个很多人问的jar 包查找调用链相关的点:如果你拿到一个JAR不知道主类在哪,我通常会先看清单,然后直接反编译整个包,搜索所有带public static void main(String[] args)签名的方法。CFR反编译出来之后用IDE的全局搜索,一般几分钟就能定位。这也是搞二次开发时最常用的手法之一。
写在最后的一些大实话
这个报错看起来小,背后涉及的其实是“JAR包结构、清单规范、构建工具配置”这几块知识。我在实际项目里见过不少同事在这个坑上反复栽跟头,总结下来就是一个习惯问题:每次打包完成后,先跑一遍unzip -p xxx.jar META-INF/MANIFEST.MF看一眼,确认Main-Class存在,再执行java -jar。即使不是可执行JAR,也顺手看一眼,心里有数。
另外一个小建议:把常用命令保存成脚本或者笔记,尤其是jar cfm的参数顺序,这种冷门参数一天不用就忘。我第一次手动补清单时,把cfm和cfe的顺序记反了,折腾了半小时才明白是怎么回事,现在每次都会先jar --help确认一遍。希望能帮你少走我走过的弯路。