干Java这行的,几乎没人能绕开“打jar包”这三个字。不管是把自己写的工具类发给同事,还是把一个Spring Boot服务部署到Windows服务器上,最后一步基本都得落到“怎么打出一个能跑的jar包”上。但我发现一个很有意思的现象:同样问“Intellij怎么打jar包”,完全可能是三种不同的人在问——用普通Java项目的人想打一个能双击运行的小工具;用Maven构建的Spring Boot开发者想把整个服务打包部署;还有一类人手里的项目是接了京东SDK、阿里云SDK这类外部JAR的,光把代码打完还不够,外部依赖也得跟着进去。这三种场景在IDEA里的操作路径完全不同,网上很多教程只讲了其中一种,读者照着做就踩坑。
这篇文章我就一次性把IDEA里打jar包的几种主流姿势讲清楚,包括图形界面操作、Maven命令、外部本地JAR的三种处理路线,以及我自己在项目里遇到过的各种报错和排查思路。无论是刚接触IDEA的Java学习者,还是准备把Spring Boot服务扔到Windows服务器上运行的开发,应该都能从里面找到能直接抄作业的内容。
1. 先把概念理清楚:你要打的到底是什么“包”
1.1 JAR、可执行JAR、Fat JAR、Spring Boot JAR的区别
很多新手对“jar包”的理解其实停留在“它是一个压缩文件”的层面,这没错,但不够。JAR(Java ARchive)本质上就是一个ZIP格式的压缩包,里面装的是编译后的.class字节码文件、配置文件、资源文件,以及一个描述自身信息的META-INF/MANIFEST.MF清单文件。普通JAR包就好比一个零件仓库,里面的类可以被别的项目引用,但它自己不知道怎么启动。
可执行JAR和普通JAR最大的区别,就是在MANIFEST.MF里多了一行Main-Class配置,相当于在仓库门口挂了个“从这里进去找门卫”的牌子。JVM执行java -jar xxx.jar的时候,会先去读这个Main-Class,然后找到对应的main方法启动程序。
Fat JAR(也叫uber JAR)则是把项目依赖的所有第三方JAR全部解压后重新合并到一个大JAR里,这样整个程序就只有一个独立的文件,拷贝到任何装了JDK/JRE的机器上都能直接跑。Spring Boot的可执行JAR外观上也是单个文件,但内部结构不太一样,它把依赖放在BOOT-INF/lib目录下,类和资源放在BOOT-INF/classes下,用自定义类加载器去加载,所以你既不能把Spring Boot的JAR当成普通JAR塞到别的项目的classpath里,也不能简单粗暴地把它的内容解压后合并到别的包里。
了解这些之后,你就能明白一个关键结论:普通Java项目用IDEA的Artifact功能打包没问题,但Spring Boot项目最好老老实实用Maven/Gradle插件打,因为IDEA的Artifact打包根本不认识Spring Boot的启动逻辑。
1.2 为什么IDEA不能对“所有项目”一键打包
IDEA界面上的确有个Build → Build Artifacts菜单,很多新手点了之后发现要么没反应,要么打出来的包运行报错。原因是IDEA自己并不是一个构建工具,它只是一个集成开发环境。对于普通的纯Java项目,IDEA可以通过Artifact的配置把编译产物组织成JAR;但对于复杂项目,真正负责编译、依赖解析、打包的是Maven或Gradle,IDEA只是把它们的界面和操作折叠成了几个按钮。
所以你在问“Intellij怎么打jar包”之前,先看一眼项目根目录有没有pom.xml或者build.gradle文件。有的话,你就已经在用Maven/Gradle的世界里了,打包方式应该围绕这两个工具来;只有完全没有构建脚本的纯Java项目,才值得去用IDEA的Artifact图形界面。
2. 普通Java项目:用IDEA图形界面打出可执行JAR包
2.1 前置准备:先把Project SDK和编译输出确认好
打开IDEA后第一件事,不是直接去配Artifact,而是确认项目的SDK。按Ctrl+Shift+Alt+S(Windows)或者Cmd+;(Mac)打开Project Structure,在Project选项卡里确认Project SDK选的是你本机装好的JDK,Project language level也要和代码里用的语法版本匹配。如果你代码里用了Java 11的var关键字,但SDK选的是Java 8,编译阶段就会报错。
另外建议先执行一次Build → Build Project(快捷键Ctrl+F9),确保当前代码能完整编译通过。很多打包失败其实根本不是打包配置的问题,而是代码本身编译不过,IDEA会直接中断后续流程。这个习惯能帮你把问题定位范围缩小很多。
2.2 新建Artifact:完整菜单操作与参数解释
确认SDK没问题之后,开始配置Artifact。流程如下:
File → Project Structure,左侧选择Artifacts。- 点击中间的
+号,选择JAR → From modules with dependencies。 - 弹出的窗口里,
Module选择你的主模块,Main Class点击右侧的文件夹图标,选择包含main方法的类。 - 下面有个
JAR files from libraries的选项,默认是extract to the target JAR,意思是把第三方依赖解压后和你的代码合并进同一个JAR,这也就是前面说的Fat JAR方案。如果你希望依赖保持原样、以lib目录形式放在最终JAR旁边,就选copy to the output directory and link via manifest。 - 在
Directory for META-INF/MANIFEST.MF这一栏,IDEA会默认指向src/main/java,这是很多新手踩坑的高发位置。最好手动改成src/main/resources,否则生成出来的MANIFEST.MF会出现在源码目录里,有时还会污染Git提交,看着相当难受。
设置完成之后,界面下方会生成一个Available Elements列表,你可以在这里勾选哪些模块内容进入JAR、哪些排除。如果项目里有测试代码,记得把测试相关的模块从列表里移除,否则打出来的包会带着一堆测试类。
2.3 触发构建:Build → Build Artifacts
配置完成后回到主界面,执行Build → Build Artifacts,选择你刚创建的Artifact名称,再选Build或Rebuild(Rebuild会清空缓存重新构建,通常更干净)。构建完成后,在Project Structure里设置的Output directory(默认是项目的out/artifacts/项目名_jar目录)下就能找到生成的JAR文件。
如果最终JAR大小只有几个KB,说明依赖没有一起打进去,多半是JAR files from libraries那边选错了选项;如果你选了extract to the target JAR,最终产物会比较大,但单独这一个文件就能跑。
验证方式很简单,打开命令行,cd到输出目录,执行java -jar 你的包名.jar。如果程序正常启动、功能正常,说明这个JAR没问题,可以发给别人用了。
2.4 图形界面打包的坑:签名文件冲突和重复类
IDEA自带的Artifact Fat JAR方案,在实际工程里用得多了就会发现两个问题。
第一个是签名文件冲突。很多第三方JAR(比如某些官方SDK)自带的META-INF目录里有.SF、.DSA、.RSA签名文件,IDEA把所有依赖解压合并进一个JAR时,这些签名文件会互相覆盖。最终表现就是运行时报SecurityException: Invalid signature file digest for Manifest main attributes。解决办法是构建完后手动删除JAR里META-INF下多余的签名文件,或者在Artifact配置里把对应依赖排除掉、只保留一个。
第二个是重复类覆盖。如果两个依赖JAR里存在同名类,IDEA合并时无法感知优先级,谁先解压谁就生效,结果就是运行期出现诡异的行为或直接NoSuchMethodError。IDEA的图形界面没有提供精细化的依赖冲突管理能力,遇到这种场景,最靠谱的还是切换到Maven的shade插件去控制。
3. Spring Boot / Maven项目打包:别再用Artifact了
3.1 Maven生命周期:clean、package、install之间怎么选
只要项目里有pom.xml,先忘掉Build Artifacts这个选项,直接走Maven的生命周期。Maven打包有明确的生命周期阶段,常用的是clean、package、install三个。
mvn clean负责删除target目录下之前的所有构建产物,避免旧文件残留影响结果;mvn package负责把项目打成JAR或WAR包,输出到target目录;mvn install在package基础上,把构建产物安装到本地Maven仓库(默认在用户目录的.m2/repository下),这样其他本地项目就能以依赖方式引用你打的包。
实际发布Spring Boot服务时,我推荐用mvn clean package,保证从零开始构建。如果你只想打一个包然后部署到服务器,package就够了,不需要install。
3.2 在IDEA里执行Maven打包:右侧面板还是Terminal
IDEA提供了两种执行Maven命令的方式。
第一种是右侧的Maven工具窗口,展开Lifecycle目录,先双击clean,再双击package。这种方式对新手最友好,因为执行结果会以清晰的面板展示,点击某个报错可以直接跳到对应代码。
第二种方式是在IDEA底部自带的Terminal里直接敲mvn clean package,效果完全一样。我个人更推荐第二种,因为习惯了命令行之后,你在本地、在服务器、在CI环境里的操作是一致的,不用维护两套心智模型。另外在Terminal里执行还能顺手加参数,比如跳过测试的-DskipTests,比在Maven面板里点来点去快得多。
打包完成后,target目录下会出现两个JAR文件:一个以-plain结尾(或者叫-original,取决于插件配置),一个不带后缀。带后缀的那个才是Spring Boot的可执行JAR,注意区分,别发错了。
3.3 打包前必改的pom.xml配置
Spring Boot项目如果直接在pom.xml里什么都不配就执行mvn package,大概率也能打出一个可执行JAR,因为spring-boot-starter-parent已经帮你配好了spring-boot-maven-plugin。但有几个细节建议手动确认一下。
第一是finalName。在<build>节点下加一个<finalName>,可以自定义打出来的JAR文件名。比如:
<build> <finalName>user-center</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.example.UserCenterApplication</mainClass> </configuration> </plugin> </plugins> </build>mainClass建议显式指定。如果项目里存在多个带main方法的类(比如一个启动类、一个数据初始化工具类),插件自动检测时有可能选错,最后打出来的包启动后报错或行为不符合预期。
第二是跳过测试。默认执行mvn package时会跑一遍所有单元测试,如果测试有环境依赖(比如连数据库、调外部接口),打包过程可能卡几分钟甚至直接失败。不想跑测试就用:
mvn clean package -DskipTests-DskipTests是编译测试类但不执行;如果想连测试代码编译都跳过,用-Dmaven.test.skip=true。
3.4 部署前最后验证:本地java -jar跑起来
打包完成后不要急着扔到服务器上,先在本机验证一次。命令行执行:
java -jar target/user-center.jar看到类似Started UserCenterApplication in xx seconds的日志,说明包没问题。这个验证动作虽然简单,但能提前暴露90%的“包打坏了”问题,比如主类选错、依赖缺失、资源文件没打进去等。
在Windows服务器上运行时,建议把启动和停止写成脚本。start.bat内容大概是这样:
@echo off start "user-center" javaw -jar user-center.jar --server.port=8080用javaw而不是java可以避免启动后命令行窗口一直挂着;要停止服务的话,在任务管理器里按照进程名结束对应Java进程,或者用netstat -ano找到占用8080端口的PID再taskkill /PID <pid> /F。这里有个容易被忽略的点:服务器上必须已经安装JDK并配好JAVA_HOME环境变量,单纯装个JRE在某些情况下跑Spring Boot的可执行JAR容易出问题,最好统一用JDK环境。
4. 引入本地JAR包后怎么打:外部依赖的三条处理路线
4.1 场景说明:官方SDK不在Maven中央仓库怎么办
实际项目中经常遇到这种问题:采购了某个平台的服务,对方给了一个JDK的SDK压缩包,里面是一堆JAR文件,但Maven中央仓库里根本找不到对应的依赖坐标。京东的某些对接SDK、老牌硬件厂商的串口通信SDK,都有这种情况。这时候你的项目代码能写出来,是因为IDEA在编译期能找到这些JAR,但最终打出来的JAR包能否带着这些依赖一起跑,取决于你用了哪种导入方式。
4.2 路线一:把本地JAR安装到Maven本地仓库(推荐)
最干净的做法是把本地JAR当作一个标准Maven依赖,手动安装到本地仓库:
mvn install:install-file \ -Dfile=jd-open-sdk-1.0.jar \ -DgroupId=com.jd \ -DartifactId=jd-open-sdk \ -Dversion=1.0 \ -Dpackaging=jar执行完这条命令之后,本地Maven仓库的com/jd/jd-open-sdk/1.0目录下就会出现对应的JAR文件。然后你在项目的pom.xml里像引用普通依赖一样写:
<dependency> <groupId>com.jd</groupId> <artifactId>jd-open-sdk</artifactId> <version>1.0</version> </dependency>这样后面的一切行为(编译、打包、传递依赖)都跟在中央仓库拉下来的依赖没有任何区别,Spring Boot插件会把这个依赖打进去。这是最稳妥、最不容易出幺蛾子的方案。
4.3 路线二:把JAR放在项目lib目录,用system scope
有些朋友拿到SDK后习惯直接丢到项目的lib目录下,然后在IDEA里右键Add as Library,代码编译当然没问题,但Maven打包时根本不认识这个依赖,最终JAR运行时会报NoClassDefFoundError。
要让Maven在打包时把这个本地JAR也带进去,可以在pom.xml里这样配:
<dependency> <groupId>com.jd</groupId> <artifactId>jd-open-sdk</artifactId> <version>1.0</version> <scope>system</scope> <systemPath>${project.basedir}/lib/jd-open-sdk-1.0.jar</systemPath> </dependency>systemscope的意思是“依赖在本地,不走仓库”,配合systemPath指向具体文件,Maven编译时会用到它。再配合spring-boot-maven-plugin的includeSystemScope配置,就能把它打进最终的可执行JAR:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <includeSystemScope>true</includeSystemScope> </configuration> </plugin>但这条路线有两个隐患:一是systemscope的依赖在发布到私有Maven仓库时不会被跟随传递,团队里其他人拿到项目后如果本地没有lib目录下的JAR,编译直接失败;二是如果以后要换到Nexus私服或者中央仓库,还得改回正常依赖。它适合临时、单机、个人项目,团队合作时强烈不推荐。
4.4 路线三:自己搭个私有Maven仓库,把SDK传上去
如果团队不止你一个人开发,还要配合CI/CD流水线自动打包,最佳方案是搭建一个简单的Nexus或Artifactory私有仓库,然后把第三方SDK通过Web界面上传,或者用mvn deploy:deploy-file推送上去。这样所有团队成员在pom.xml里配置好私服地址后,都能像用中央仓库依赖一样正常引用,打包、发布全链路都不会断。虽然搭建私服前期会花一点时间,但长远看是省事的。
4.5 外部JAR在运行期报ClassNotFoundException的排查顺序
代码在IDEA里跑得好好的,打成JAR后一到服务器就报ClassNotFoundException,这种情况我遇到过无数次。排查思路按顺序来:
第一,jar tf 你的包名.jar查看外部SDK的类是否真的在最终JAR里。如果不在,回到打包方式上找原因,多半是依赖没被插件识别。
第二,确认本地JAR的版本和服务器上运行的JAR版本一致。有时给客户的是旧包,本地坑了半天新的SDK功能,服务器却还在加载旧包。
第三,确认运行时有没有使用外部的CLASSPATH环境变量覆盖了JAR内部依赖。有些脚本里会写set CLASSPATH=xxx,这会导致JVM优先加载外部类,出现预料之外的冲突。
5. 高频问题排查实录:打jar包路上我踩过的坑
5.1 Lombok报错:requires enabled annotation processing
最近好几个朋友都在问这个问题,IDEA里代码一切正常,执行mvn clean package时却报错,大概意思是当前环境没有启用注解处理,而Lombok依赖又必须要注解处理才能生成代码。这个问题的根源是IDEA的Annotation Processors设置默认没有开启,或者项目迁移到新电脑后配置丢了一部分。
解决步骤是:File → Settings → Build, Execution, Deployment → Compiler → Annotation Processors,勾选Enable annotation processing,然后点击Apply,重新构建。如果项目里同时存在多个模块,记得每个模块都要确认Annotations Profile里的配置正确。
这个问题还有个变种:Lombok版本和JDK版本不兼容。比如JDK 17之后,老版本的Lombok(1.18.20以下)无法正常工作,表现为编译报错或代码里使用的@Slf4j生成的log变量全部不可见。升级Lombok依赖到较新版本,或者统一JDK版本,都能解决。
5.2 打出来的JAR双击运行闪退
Windows下双击JAR包运行闪退,绝大多数不是打包的问题,而是环境或执行方式的问题。先打开命令行,切换到JAR目录,手动执行java -jar xxx.jar,这样能看到完整的报错信息。
最常见的原因有三个:一是Main-Class没配置或配错,JVM提示Could not find or load main class;二是JDK版本不匹配,代码用了高版本特性但运行环境的JDK太老;三是程序里引用了外部文件(比如配置文件、模板目录),双击运行时当前工作目录不确定,程序按相对路径找不到文件直接退出。最后一种情况建议用java -jar xxx.jar配合--spring.config.location等外部参数启动,不要依赖双击。
5.3 打出来的JAR缺少第三方依赖
如果你用的是IDEA图形界面的Artifact方式,漏依赖是高频问题,尤其当项目里通过Maven引入了七八个依赖时,IDEA不一定能基于复杂依赖图生成完整的Fat JAR。稳妥的做法是给Maven项目配上maven-assembly-plugin或maven-shade-plugin。shade插件配置参考:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.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.Main</mainClass> </transformer> </transformers> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin>这个配置里有两个关键点。一个是ManifestResourceTransformer,它会帮你在合并依赖时生成正确的Main-Class。另一个是filters里的排除签名文件逻辑,加了之后基本不会碰上前面说的SecurityException。
5.4 资源文件(application.yml、数据库脚本等)没有进入JAR
Spring Boot项目正常用mvn package打出来的JAR,application.yml等resources目录下的文件会自动打进BOOT-INF/classes。但如果你用的是IDEA的Artifact方式,资源文件不会自动包含,需要在Artifact配置的Available Elements里把resources目录手动添加进去,否则运行时就报找不到配置文件。
还有一种情况是资源文件打进去了,但运行时加载的是外部配置而不是内部配置,导致配置不生效。Spring Boot的配置加载规则里,外部配置的优先级高于JAR内的配置。如果你在部署命令里写了--spring.config.location=file:../config/application.yml,那么JAR里同名的配置不会生效,这不是打包的问题,是配置优先级的问题,排查时别搞混。
5.5 MANIFEST.MF被锁定无法修改
IDEA重新打包时,偶尔会报Invalid jar file或者提示META-INF/MANIFEST.MF已经被占用。这通常都是IDEA缓存、或者是上一次打包时JAR文件没释放导致的。把out目录和target目录手动删除,然后File → Invalidate Caches / Restart清一下IDEA缓存,基本都能解决。
5.6 一个项目里多个Main-Class时打出了错误启动类
在微服务多模块项目里特别容易出现。Maven的spring-boot-maven-plugin在自动识别启动类时,如果找到了多个候选,可能打包成功但运行时不走你预期的那个入口。解决方式就是在插件配置里显式指定mainClass,不要指望自动检测。
5.7 服务器上运行提示“缺少主清单属性”
这个报错英文是no main manifest attribute,出现频率极高,一般是因为你打的JAR只是一个普通JAR包,MANIFEST.MF里没有Main-Class信息。Spring Boot的可执行JAR和普通JAR的差异就在这。排查方式:用压缩软件打开JAR,查看META-INF/MANIFEST.MF内容,没有Main-Class就把包重新打一遍,或者直接用Spring Boot插件打包。
5.8 本地能跑,服务器上就是不行的奇怪差别
这类问题很多时候不是代码的问题,而是环境不一致。本地Windows开发机和Linux服务器上文件路径分隔符不同(Windows用\,Linux用/),如果代码里用字符串拼接路径,到Linux上就容易找不到文件。另一个经典坑是字符集,Windows默认GBK,Linux默认UTF-8,配置文件里有中文注释或中文内容时,在服务器上读出来可能是乱码。建议项目里所有资源文件统一用UTF-8编码,JVM启动参数里加上-Dfile.encoding=utf-8。
| 常见报错 | 可能原因 | 解决办法 |
|---|---|---|
no main manifest attribute | JAR不是可执行JAR,MANIFEST.MF缺少Main-Class | 检查打包方式,使用Spring Boot插件或配置Main-Class |
Could not find or load main class | Main-Class配置错误或类名拼写不对 | 在插件里显式指定正确的主类全限定名 |
ClassNotFoundException/NoClassDefFoundError | 依赖没有包含在最终JAR中 | 检查依赖作用域,给外部JAR做install-file,或使用shade插件 |
SecurityException: Invalid signature file digest | 多个依赖的签名文件冲突 | 打包时排除META-INF下的.SF/.DSA/.RSA文件 |
| Lombok编译报错 | 注解处理未开启或Lombok与JDK版本不兼容 | Settings里勾选Enable annotation processing,升级Lombok |
| JAR包双击闪退 | 运行时环境问题或依赖缺失 | 命令行运行查看报错,检查JDK版本和工作目录 |
6. 一点个人习惯分享
最后说一个我自己的习惯。我早期也喜欢在IDEA里点各种图形界面按钮去Build Artifact,后来项目越做越复杂、依赖越来越多,图形界面就越发不好用了。现在我的标准流程基本固定成了:代码写完、本地跑通,然后在IDEA的Terminal里敲mvn clean package -DskipTests,再用java -jar验证一次,最后把JAR丢到服务器上。中间偶尔需要引入本地SDK,就先把SDK安装到本地Maven仓库,再在pom.xml里加依赖,整个过程非常机械、不容易错。
如果你也是刚接触IDEA的Java开发者,建议也尽早切换到这种“Maven生命周期 + 命令行验证”的工作流上。IDEA的图形界面当然可以用,但只建议用在纯Java小工具项目上。至于那些网上流传的“一键打包神器”“某某版本特殊激活方式”,我劝你别花时间折腾,IDEA社区版(Community Edition)在官方渠道就能免费下载,日常开发、打包、跑Maven项目完全够用,把精力放在熟悉构建工具和排查实际问题上去,长期看收益大得多。