简介:IDEA打包可执行jar文件时,“找不到或无法加载主类 main”是高频报错之一,这份PDF面向Java开发者与Maven项目使用者,系统梳理从原因定位到实际解决的完整路径。文档重点涵盖三种方案:在pom.xml中配置maven-jar-plugin并声明主类全限定名、检查与修正Windows系统CLASSPATH环境变量(包括直接删除该变量以恢复自动类路径)、以及通过Project Structure的Artifacts功能构建可运行jar。包体为1个PDF文件,容量约258KB,内容紧凑,附有可复制的XML配置示例与分步操作说明。资源发布至今已有20876人浏览学习,除了打包阶段的排错,还补充了已编译class文件仍报找不到类时的环境变量处理技巧,对需要快速定位配置问题、理解Jar清单与类路径机制的开发者而言,是一份可直接对照操作的实用参考。
1. IDEA打包jar报“找不到或无法加载主类”:先分清三类原因再动手
在 IntelliJ IDEA 里点一下 Build Artifacts,看到控制台把 main 方法的结果打印出来,你以为这个 jar 已经能交付了,结果换台机器java -jar xxx.jar,迎面就是“错误: 找不到或无法加载主类”。这个问题不是玄学,也不是 IDEA 坏了,而是可执行 jar 的三道门槛里至少有一道没到位:MANIFEST.MF 里有没有写对 Main-Class、这个类在 jar 里到底存不存在、它依赖的库有没有一起带上。本文就沿着这三条线把 IDEA 打包 jar 的完整路径拆开,从原理到最小复现,再到 Maven 脚本和一套能落地的校验命令,新手照着做能交差,熟手也可以拿来当排查清单用。
2. 从“能编译”到“能运行”:可执行 jar 的构成与最小复现
2.1 java -jar 到底在看什么:Main-Class、Class-Path 与 jar 包结构
很多人有个误解:在 IDEA 里能运行,就等于打出来的 jar 也能运行。这两件事的类加载路径完全不同。IDEA 运行程序时,是把编译后的target/classes目录和你在 Project Structure 里导入的依赖 jar 一个不落地拼进 classpath,然后 JVM 按 classpath 去加载com.example.demo.Main;而java -jar启动规则是另一套:JVM 只打开你指定的 jar 文件,读取里面的META-INF/MANIFEST.MF,先看Main-Class属性,再去 jar 包内部找这个类。
也就是说,jar 包本质是个 zip,里面装的是目录结构化的.class文件。JVM 对“可执行 jar”的期望是:入口类的全限定名写在清单文件里,入口类本身以对应路径存在 jar 内。如果这两个条件不满足,它就只能对着命令行抛一句“找不到或无法加载主类”。至于你用 IDEA 右键导入的那些外部依赖 jar,它们叫编译期依赖,只在你 IDE 的 classpath 里,不会自动进你的输出物,除非打包时明确处理。
顺便提一句:java -jar模式下,命令行里的-cp参数会被忽略,jar 包的 Class-Path 才有效。这是很多人第一次翻车的地方——本地怎么加 classpath 都没用,因为 jar 内部的清单没有告诉你 JVM 该去哪儿找包。理解了这一层,后面所有坑都能对上号。
2.2 复现报错的最小工程:用 IDEA 的 Artifacts 打一次 jar
要搞懂这个问题,最快的办法是自己复现一次。新建一个空 Java 工程,写一个最简单的入口类:
package com.example.demo; public class GreetingMain { public static void main(String[] args) { String name = args.length > 0 ? args[0] : "IDEA"; System.out.println("hello, " + name); } }逻辑很简单:main 方法接收一个可选参数,没有就用默认值。注意这个类带了包名com.example.demo,这个包名后面会是 MANIFEST 里 Main-Class 的关键部分。
接着在 IDEA 里按下面步骤操作:
File → Project Structure → Artifacts,点+,选JAR → From modules with dependencies。- Main Class 选
com.example.demo.GreetingMain。 - 注意
Directory for META-INF/MANIFEST.MF这一项,千万别指到src/main/java下,否则会污染源码目录;我一般新建src/main/resources/META-INF。 - 回到编辑器,
Build → Build Artifacts → Build,输出文件默认在out/artifacts/<项目名>_jar/下。
java -jar out/artifacts/demo_jar/demo.jar很多人在这一步就会看到“错误: 找不到或无法加载主类”。原因可能是 IDEA 给你生成的 MANIFEST 写错了入口,也可能你只拷走了单个 jar、漏掉了同目录的lib/文件夹。IDEA 的默认布局会把依赖放在 jar 外的lib目录,再靠 MANIFEST 里的Class-Path指过去,这意味着整个输出目录必须一起挪,不能只搬 jar。
2.3 先验证再谈修复:用 jar tf 和 unzip -p 看清单文件
报错之后别急着改配置,先把 jar 里到底有什么看一遍。jar 就是 zip,所以jar tf和unzip都能用。我一般固定跑这一组命令:
jar tf demo.jar | grep -E "META-INF/MANIFEST.MF|com/example/demo/GreetingMain.class" unzip -p demo.jar META-INF/MANIFEST.MF | tr -d '\r'第一条命令检查两件事:清单文件在不在、主类对应的.class文件在不在。第二条命令直接看 MANIFEST 内容,tr -d '\r'是为了去掉 Windows 换行符,避免后面用脚本解析时踩到回车符的坑。
如果看到Main-Class: com.example.demo.GreetingMain且.class文件路径确实是com/example/demo/GreetingMain.class,那问题多半出在依赖上;如果 Main-Class 后面是空的,或者写的是GreetingMain这种没有包名的简写,问题就出在清单文件上。这一步做完,锅在谁身上基本就清楚了。
3. 改对 MANIFEST.MF:主类配置的四种写法与三个坑
3.1 包名别落下:Main-Class 必须写全限定名
最常见的低级错误,是 Main-Class 只写了类名,没写包名。比如你的类是com.example.demo.GreetingMain,但 MANIFEST 里写的是Main-Class: GreetingMain。JVM 在 jar 内部找GreetingMain.class,当然找不到,因为实际路径是com/example/demo/GreetingMain.class。
正确写法就一行:
Manifest-Version: 1.0 Main-Class: com.example.demo.GreetingMain注意两点:第一,不能带.class后缀;第二,如果类在默认包下,直接写类名没问题,但只要你在真实项目中,几乎都会有包名,所以这个坑比想象中高发。还有一种写法是复制 IDEA 运行配置里的“Main class”下拉框值,那个值通常是对的,但手工敲的时候容易漏,漏了就从“能编译”变成“运行不了”,而且编译期不会给你任何提示。
3.2 main 方法的签名为什么“编译能过、运行报错”
Java 编译器检查的是语法,不会校验“这个类适不适合当程序入口”。比如下面这三个写法都能编译通过:
public class WrongMain { public void main(String[] args) { } }public class WrongMain2 { public static void main() { } }public class WrongMain3 { static void main(String[] args) { } }但 JVM 查找入口时要求的是精确签名:public static void main(String[]),一个修饰符都不能少。void返回值、String[]参数、public static缺一不可。否则你会收到“错误: 在类 com.example.demo.WrongMain 中找不到 main 方法”。
用javap可以验证编译后的真实签名:
javap -classpath demo.jar com.example.demo.GreetingMain它会打印出类的公开方法列表,你看一眼有没有public static void main(java.lang.String[])。这个方法比反编译 jar 更快,不需要打开任何工具,命令行一行就解决。以后但凡怀疑入口签名有问题,先跑 javap,别靠肉眼猜。
3.3 在 IDEA 里设置 Main Class 的生效边界
IDEA 的 Artifacts 配置界面里有一个 Main Class 选择框,很多人以为选了就万事大吉。实际上它的作用是帮你生成 MANIFEST,而生成时机是 Build Artifacts 的那一刻。如果你后来改了入口类的包名、改了类名,或者把入口类从一个模块移到了另一个模块,旧 jar 里仍保留旧配置。
我遇到过最典型的情况:项目重构后把Main类从demo.old.Main挪到demo.new.Launcher,IDEA 里运行没问题,因为运行配置自动修正了;但 Artifacts 配置里的 Main Class 还是旧值,重新 Build 出来的 jar 依然写着Main-Class: demo.old.Main,自然找不到。
解决办法:每次大改结构后,回 Project Structure 里确认一遍 Artifacts 的 Main Class。另外,如果你同时用 Maven 或 Gradle 构建,IDEA Artifacts 的产物在out/,Maven 的产物在target/,两者没关系,别搞混。
3.4 手动修改 MANIFEST.MF:用 jar 命令打补丁
如果你拿到了一个别人打好的 jar,不想重新打包,只想临时修正 Main-Class,可以手写一个 manifest 文件,然后用jar命令更新进去。
printf 'Main-Class: com.example.demo.GreetingMain\n\n' > manifest.txt jar ufm demo.jar manifest.txtufm三个字母解释一下:u是更新已有 jar,f指定 jar 文件名,m表示使用外部 manifest 文件。这个命令会把manifest.txt里的内容合并进 jar 内部的META-INF/MANIFEST.MF,效果等价于重新打包一次,但快得多。
这里有一个必须注意的细节:manifest.txt最后要有换行,很多手写文件漏了结尾回车,导致最后一行属性被忽略,jar命令也不报错,你查 MANIFEST 却看不到 Main-Class。所以我上面在printf里专门加了两个\n,一个结束本行,一个保证文件以空行收尾。这个习惯能帮你避开一个很隐蔽的坑。
4. 依赖没带上:从 java -cp 到带依赖的 fat jar
4.1 最朴素的验证:用 java -cp 把 classpath 摆平
如果 MANIFEST 和主类签名都没问题,但运行还是报“找不到或无法加载主类”,下一个嫌疑就是依赖缺失。主类本身可能没问题,但它引用的第三方类在 jar 里没有,JVM 在加载主类时解析符号引用失败,照样会报错。
这时候最直接的验证办法是绕开java -jar,自己显式拼 classpath:
mvn dependency:copy-dependencies -DoutputDirectory=lib java -cp "target/classes:lib/*" com.example.demo.GreetingMain第一条命令把 Maven 依赖拷到lib目录,第二条命令把编译产物和依赖一起放进 classpath,然后手动指定完整主类名。lib/*这个通配符是 Java 6 以后支持的 classpath 通配,不是系统 shell 展开的,别加引号反而会被 shell 拆成多个参数。
如果这样能跑起来,就说明代码本身没问题,问题100%在打包环节:依赖没进 jar,或者 MANIFEST 的 Class-Path 没指向依赖所在位置。
4.2 用 maven-jar-plugin 把依赖写进 Class-Path
解决依赖问题有一个过渡方案:用maven-jar-plugin生成标准 jar,再用maven-dependency-plugin把依赖拷贝到 jar 旁边的lib目录,最后让 MANIFEST 里写入Class-Path。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.4.1</version> <configuration> <archive> <manifest> <mainClass>com.example.demo.GreetingMain</mainClass> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> </manifest> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> </configuration> </execution> </executions> </plugin>这里addClasspath让 Maven 自动生成 Class-Path 属性,classpathPrefix告诉它依赖都放在lib/相对目录下。打包后target/下会出现demo.jar和lib/文件夹,两个必须一起拷走。
这个方案的问题很明显:交付物不是一个文件,而是多个文件;目录结构一乱,Class-Path 就失效。所以我只在快速验证时用,正式交付我还是偏好下一步的 fat jar。
4.3 用 maven-assembly-plugin 打带依赖的 fat jar
fat jar 的意思是所有依赖的.class都解开后重新塞进同一个 jar,这样最终只有一个文件。maven-assembly-plugin是干这件事最省事的方式:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.6.0</version> <configuration> <archive> <manifest> <mainClass>com.example.demo.GreetingMain</mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>执行:
mvn clean package -DskipTests产物在target/下,文件名是<项目名>-jar-with-dependencies.jar。这个 jar 可以直接拷走,不需要额外带任何目录。
参数说明:descriptorRefs里的jar-with-dependencies是插件内置的描述符,含义就是把 project 的依赖全部解压合并;phase=package绑定了生命周期节点,跑mvn package就会自动触发,不用额外执行assembly:single。如果你的项目里某些依赖有签名文件(META-INF/*.SF),合并时会有安全警告,可以在插件配置里加filters排除掉,但一般命令行工具不用管。
4.4 Spring Boot 项目打包:repackage 与 Main-Class 的自动改写
如果你用 IDEA 建的是 Spring Boot 项目,上面所有方案都不适用。Spring Boot 的 jar 是特殊结构,入口类不是你的业务 Main,而是它的启动器。
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>这个插件在你的 Mavenpackage阶段做一次 repackage:把项目的普通 jar 改造成 Spring Boot 可执行 jar。打完之后看 MANIFEST,你会发现 Main-Class 是org.springframework.boot.loader.JarLauncher,而你的业务主类被写进了另一个属性Start-Class。
很多人第一次看到这里就以为打错了,其实是正常的。Spring Boot 的启动机制是:JVM 先加载 JarLauncher,由它去处理BOOT-INF/classes和BOOT-INF/lib的内部类路径,再反射调用你写的Start-Class。所以 Spring Boot 项目里,依赖永远不会丢,因为它的 loader 设计就是专门解决这个问题的。
如果你不想用 Spring Boot 插件的 repackage 而想用 assembly,能打出来,但跑起来会有一堆问题,不值得。Spring Boot 项目直接用插件默认配置就好,IDEA 的 Artifacts 对它基本无用。
5. IDEA 打包 jar 避坑检查单:4 条血泪经验
5.1 现象:IDEA 里能跑,命令行 java -jar 就报错
这是我看到最多的求助帖:IDE 里运行一切正常,打成 jar 后命令行立刻“找不到或无法加载主类”。
原因通常是两个。一是 IDEA 运行时用的 classpath 是编译产物的路径加依赖列表,跟 jar 没关系;二是 IDEA Artifacts 默认把依赖放 jar 外的lib/目录,只拷单个 jar 等于裸奔。
解决:先按 2.3 的命令查 MANIFEST,确认 Main-Class 正确;再把out/artifacts/整个目录拷走,包括lib/;如果交付要求只是单个文件,改用 4.3 的 assembly 插件重新打 fat jar。
5.2 现象:用 Maven 打包后,自定义 MANIFEST 被覆盖
你在src/main/resources/META-INF/MANIFEST.MF里手写了 Main-Class,跑完mvn package打开生成的 jar,发现配置没了,或者变成 Maven 默认的版本号。
原因:Maven 的maven-archiver会重新生成 MANIFEST,默认不读 resources 目录下的那个文件。你放在 resources 下的 MANIFEST 会被原样复制到 jar 里,但 Maven 自己生成的清单在打 jar 时会被maven-jar-plugin覆盖掉或合并掉。
解决:别在 resources 下手写 MANIFEST,把入口配置写进maven-jar-plugin的<manifest>配置段,这是 Maven 的正式入口;resources 下那份只用来放自定义属性。如果你真的很依赖手写文件,用maven-archiver的manifestFile参数指定来源,但优先级逻辑很绕,不建议第一次就碰。
5.3 现象:主类加载失败,错误信息却是 ClassNotFoundException 或 NoClassDefFoundError
“找不到或无法加载主类”是一个统称,它背后的原始异常可能是ClassNotFoundException、NoClassDefFoundError。如果你的主类在静态代码块里引用了外部类,而那个外部类不在 jar 里,异常会在入口类加载阶段提前抛出来。
原因:JVM 加载主类时,如果主类的常量池里引用了一个缺失的类,且这个引用在验证或解析阶段就需要被解析,就会触发 NoClassDefFoundError。表面看是“找不到主类”,实际是主类的依赖丢了。
解决:不要只在入口类上做文章,打开 MANIFEST 的 Class-Path 检查依赖位置,或者用 4.3 的 fat jar 彻底解决。还有一种廉价排查法:把main方法里的业务代码全部注释,只留一个空壳,如果空壳能跑,说明问题就在依赖或静态初始化逻辑上。
5.4 现象:换电脑后报错 UnsupportedClassVersionError
这类场景在团队协作里很常见:你本机跑得好好的,同事拿到 jar 一跑,直接报“无法加载主类”或UnsupportedClassVersionError。
原因:你用的 JDK 版本比对方高。比如你在 JDK 17 下编译的 class 文件是 major version 61,对方只有 JDK 8,JVM 不认这个版本格式,自然加载不了。
解决:在 Maven 里显式统一编译版本,加上maven.compiler.source和maven.compiler.target,并且交付时告诉对方最低 JDK 要求。我现在的习惯是项目根 pom 里写死:
<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>这样不管本地装了多少版本的 JDK,编译出的 class 文件都是 Java 8 兼容格式。注意这只是编译器指令,不代表你能在代码里用 JDK 17 独有的 API,那种情况编译期就会报错。
6. 把“能跑”变成“能交付”:一个校验脚本与两个验证习惯
我现在每打出一个可执行 jar,都会顺手跑一个小脚本,确认三件事:清单存在、主类存在、签名正确。脚本贴在下面,保存成check-jar.sh直接用。
#!/usr/bin/env bash set -euo pipefail JAR="${1:?usage: check-jar.sh <your.jar>}" echo "== META-INF/MANIFEST.MF ==" unzip -p "$JAR" META-INF/MANIFEST.MF || { echo "FAIL: no manifest"; exit 1; } main_class=$(unzip -p "$JAR" META-INF/MANIFEST.MF \ | tr -d '\r' \ | sed -n 's/^Main-Class:[[:space:]]*//p' \ | head -n1) if [ -z "$main_class" ]; then echo "FAIL: Main-Class missing" exit 1 fi class_path="${main_class//./\/}.class" if unzip -l "$JAR" | grep -q "${class_path}$"; then echo "PASS: ${class_path} exists" else echo "FAIL: ${class_path} not found" exit 1 fi if javap -classpath "$JAR" "$main_class" \ | grep -q "public static void main(java.lang.String\[\])"; then echo "PASS: main signature ok" else echo "FAIL: main signature not recognized" exit 1 fi脚本逻辑不复杂:sed从 MANIFEST 里抽出 Main-Class 的值,main_class把包名里的点替换成路径分隔符,拿去和 jar 条目比对,最后用javap确认入口签名。Spring Boot 项目看 Main-Class 会过,但 Start-Class 没验证,配合 4.4 自己加一段即可。
两个验证习惯是我翻车多年换来的。第一个习惯:任何入口类的修改,打包前先在命令行用java -cp跑一次完整类名,确认 classpath 方案能通,再做 jar;运行时错和打包时错不要混在一个阶段查。第二个习惯:每次打完 jar,把 2.3 和 6 的命令当成打包动作的一半,脚本过了才发给别人;脚本没过,立刻查清单或依赖,不靠运气试。
IDEA、Maven、依赖管理这些工具说到底只是帮你生成文件,真正决定 jar 能不能跑的是清单和类路径。现在每遇到“找不到或无法加载主类”,我都按这套流程走一圈,基本十分钟内定位。希望帮到你。
本文还有配套的精品资源,点击获取