☰
Java jar打包成exe全攻略:jpackage与体积优化实战
2026/10/1 11:46:52 网站建设 项目流程

十年前我第一次给客户交付Java桌面工具,遇到一个至今难忘的对话:“你给我的这个jar文件,我双击了没反应。”“你装JDK了吗?”“JDK是啥?我不会装,你直接给我一个双击就能用的东西。”就这样,我被逼着把Java打包成exe这个问题研究了个底朝天。后来这活儿我干了不下百次,从Launch4j到exe4j再到jpackage,踩过各种各样的坑。这篇文章就是把这条路上的实战经验全部整理出来,从零开始,手把手教你把自己写的Java jar包变成一个Windows下双击就能运行的exe程序,覆盖工具选型、完整打包步骤、体积优化、常见坑点排查,以及最终如何用一条命令把流程固化下来。

这篇文章适合三类人:一是需要把开发好的Java桌面工具交付给非技术用户的开发者;二是公司内部做小工具、小系统,希望免安装分发的同学;三是对打包分发流程不了解,想系统补课的新手。文章默认你用的是Windows开发环境,内容以JDK 17为主,但JDK 11到JDK 21的差异点我会单独说明。

1. 分发Java程序的老大难:为什么非得是exe

先讲清楚一个最核心的问题:你不辞辛苦打包成exe,到底在解决什么?

Java程序天生是跨平台的,编译产物是class字节码,运行靠JVM解释执行。这对开发来说是好事,但对最终用户来说就是灾难。绝大多数普通用户不会安装JDK,也不想搞清楚什么是环境变量,更不应该被要求打开命令行敲java -jar。他们要的是双击图标,程序就出来,像微信、QQ那样。

在Windows生态里,exe是这个“双击就能用”的最基本载体。把Java程序打包成exe,本质上是做四件事:

  • 把用户从“自己装JDK”这件事里解放出来,JRE运行时跟程序一起分发;
  • 用Windows原生进程的方式启动程序,避免丑陋的jar文件图标和控制台黑窗;
  • 给程序附上图标、版本号、公司信息,让它看起来像个正经的商业软件;
  • 可选地提供安装向导、开始菜单快捷方式、注册表关联等Windows原生体验。

这里必须说一句公道话:并不是所有Java程序都需要打成exe。如果你做的是Web后端服务、微服务、批处理任务,部署到服务器上,那直接拿jar跑没有任何问题,反而是最标准的做法。exe化主要针对有桌面界面、需要给非技术人员使用的场景。我见过不少开发者一上来就把Spring Boot后端打成exe塞给用户,结果体积巨大、升级麻烦、日志还没地方看,纯属给自己找罪受。

另外,有一个经常被误解的点:jar和exe不是替换关系。jar是你的程序实体,exe是一个“壳”或者“启动器”,它负责把JVM拉起来,再让JVM去加载你的jar。所以“把jar打包成exe”更准确的说法是“为一个jar程序准备Windows可执行外壳”。理解了这一点,后面对工具的选择就顺理成章了。

那有没有不依赖JVM的方案?有。GraalVM Native Image可以把Java代码直接编译成Windows原生机器码,编译产物本身就是exe,运行时不依赖JVM。这个方案确实诱人,但代价是反射、动态代理、JNI这些Java特性会受限,Spring Boot等框架想要原生编译需要额外适配。对于绝大多数普通桌面工具来说,老老实实捆绑JRE再套个启动器才是最稳的路线。这个话题后面在工具选型那节还会展开。

2. 工具摸底:jpackage、Launch4j、exe4j、GraalVM怎么选

市面上把Java打包成exe的工具不少,但我实际用下来,真正值得考虑的就这么几个。选工具这件事别跟风,要看你自己的项目形态和交付要求。

2.1 jpackage:官方正道,但认识它的人不够多

jpackage是JDK官方提供的打包工具,从JDK 14引入,JDK 16开始正式可用,到JDK 17已经完全成熟。它的核心思路是把应用、依赖jar、JRE运行时打成一个完整的目录或者安装包,支持Windows的exe/msi、macOS的dmg/pkg、Linux的deb/rpm。

它最大的优势就是“官方”两个字,不需要额外的第三方许可费用,不会出现“今天能打包,明天License到期”的问题。而且它跟jlink配合得好,可以裁剪JRE体积,后面专门讲。

它的缺点也很明显:对打包exe安装包有额外依赖,Windows下需要WiX Toolset;生成的默认安装界面比较“直男美学”,如果你需要高度定制的安装体验,得花心思做后处理。但这对于绝大多数内部工具、小产品来说完全够用。

我现在的默认选择就是jpackage。原因只有一个字:稳。它是Java官方维护的,不是某个老哥的个人项目,不会出现“Java版本升级,打包工具没人维护”的尴尬。

2.2 Launch4j 和 exe4j:老牌劲旅,但定位不同

Launch4j是一个很老牌的开源工具,它做的事情是给jar套一个exe启动壳,生成的exe双击后自动去找JVM并执行你的jar。它支持自定义图标、启动画面、JVM参数,甚至支持“系统没有JVM时提示用户去下载”的逻辑。

Launch4j的做法有个关键特点是:它不捆绑JRE。它生成的exe只是启动器,真正运行还是依赖目标机器上的JDK/JRE。这跟你“给没有JDK的人用”的需求之间有落差。当然Launch4j也支持指定一个bundled JRE路径,但需要你自己把JRE复制到一个相对目录,整体体验比jpackage原生的捆绑方式繁琐一些。

exe4j是商业软件,JetBrains家的IDEA最开始就是用exe4j打包的(现在全系转向了jpackage)。它的优势在于功能细、定制能力强、官方支持好,而且生成的exe更“原生”。问题是要钱,个人版和企业版的授权费虽然不算天价,但对绝大多数个人和小团队来说没必要。

2.3 GraalVM Native Image:不走寻常路,但有硬门槛

GraalVM的Native Image是一个技术路线完全不同的方案。它不是给jar“套壳”,而是用AOT(Ahead-Of-Time)编译把Java代码直接编译成机器码。生成的exe启动速度极快、内存占用小,而且不依赖JRE,单文件体积通常20-60MB,看起来非常漂亮。

但你得提前知道它的几个大坑:

  • 反射、动态代理、JNI等运行时动态特性,默认会失效或受限,需要额外配置“反射配置”“资源配置”,这部分工作量可能比打包本身还大;
  • Spring Boot、MyBatis这类重度依赖反射的框架,做Native Image适配非常痛苦,虽然有官方Spring Native/Spring Boot 3的支持,但坑依然不少;
  • 编译时间普遍在几分钟到十几分钟,开发调试回环很慢;
  • 对Windows交叉编译支持有限,通常需要在对应平台上做编译。

我的个人建议是:普通jar项目别轻易上GraalVM,除非你有明确的“启动速度要毫秒级”“内存要严格控制”这类硬需求,或者你追求“单文件分发”到了极致。否则折腾一圈下来,你会把大量时间花在编译配置和框架适配上面。

2.4 各工具横向对比与最终建议

维度jpackageLaunch4jexe4jGraalVM Native Image
是否官方官方第三方开源商业开源/商业双轨
是否捆绑JRE支持半支持,需手动支持不依赖JRE
学习成本低低中等高
定制安装界面一般无,纯启动器强不适用
体积优化空间大,可配jlink取决于JRE取决于JRE最小
适合场景绝大多数场景目标机器已有JDK商业发行极客/高性能场景

如果你只是自己用、公司内部分发、或者简单交付给客户,我强烈推荐直接用jpackage,后面的内容全部以jpackage为基线来讲。如果你明确知道目标机器上已经装了JDK,Launch4j也可以作为临时方案用。但如果目标是做成一个精致的商业软件,且预算充足,exe4j的定制能力确实更胜一筹,只是这个需求比例太低了。

3. 前置准备:先搞出一个能双击运行的jar包

很多人卡在打包第一步,其实问题不在exe,而在他自己的jar本身就跑不起来。jpackage只是把你已经能正常运行的jar包“包一层壳”,它没有起死回生的能力。所以下面这步很关键:先确保你的jar能被java -jar正常执行。

3.1 环境要求

本机需要安装JDK 17或更高版本,并且配置好JAVA_HOME和PATH,在命令行里执行:

java -version javac -version jpackage --version

三条命令都有正常输出了,说明环境OK。如果你还在用JDK 8,建议至少先把打包这件事放在JDK 17上做,jpackage是16才开始正式的,版本太低连工具都没有。

3.2 用Maven打出可执行jar

最正统的做法是在pom.xml里配置maven-jar-plugin,指定Main-Class。以一个小记事本工具为例:

<build> <finalName>my-note</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.4.1</version> <configuration> <archive> <manifest> <mainClass>com.mynote.App</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build>

执行:

mvn clean package

然后检查target目录下生成的my-note.jar,用命令验证:

java -jar target/my-note.jar

窗口能正常弹出来,说明主类配置正确、依赖也完整。

3.3 Spring Boot项目的特殊处理

Spring Boot项目默认打的jar不是传统意义上的可执行jar,它把项目依赖放进了BOOT-INF/lib,主类是org.springframework.boot.loader.JarLauncher。用spring-boot-maven-plugin的repackage目标打成fat jar后,java -jar是可以正常跑的,而且jpackage可以直接识别这种fat jar。

推荐的配置是你在Spring Boot项目里什么都不用额外加,直接:

mvn clean package java -jar target/my-app.jar

能跑就行。需要提醒一点:千万不要画蛇添足去指定--main-class为Spring Boot的启动类,这样反而可能导致类加载问题。jpackage在处理Spring Boot fat jar时,直接用jar自带的Main-Class即可,也就是让JarLauncher去加载。

3.4 不用Maven,手工编译怎么处理

如果你的项目是个极简项目,没有Maven/Gradle,那也完全可以。目录结构大概是:

D:\my-note\ ├── src\com\mynote\App.java ├── libs\ │ ├── commons-io.jar │ └── gson.jar

手工编译:

javac -encoding UTF-8 -cp "libs/*" -d classes src/com/mynote/*.java jar --create --file my-note.jar --main-class com.mynote.App -C classes .

同样是执行前先用java -jar验证。这步做完,打包exe的前置条件就齐了。别嫌这一步啰嗦,我在实际工作中见过至少十几次“jpackage打包后运行秒退,查了半天,结果是jar本身缺依赖”的情况。基础打牢,后面才不会反复返工。

4. 正式打包:jpackage生成exe的完整命令拆解

前置条件满足之后,现在就进入核心环节。jpackage支持两种不同的产物形态,先从一个最直观的开始,再进入安装包形态。

4.1 最容易理解的形态:--type app-image

app-image的含义是生成一个完整的可运行目录,不生成安装程序。这个目录里包含了你的程序、jar、JRE运行时和启动exe。把它整个文件夹拷到别的Windows机器上,双击里面的exe就能跑。

命令如下:

jpackage --type app-image --input target --main-jar my-note.jar --name MyNote --dest dist

各个参数的含义:

  • --type app-image:指定产物类型为目录结构,这是后面所有高级打包的基础;
  • --input target:指定输入目录,jpackage会把该目录下所有文件(jar、依赖资源)拷贝进应用目录;
  • --main-jar my-note.jar:指定主jar文件名,必须是--input目录下的相对路径文件名;
  • --name MyNote:应用名称,这个会作为exe文件名和安装后的快捷方式名;
  • --dest dist:输出目录。

执行完成后,dist/MyNote/目录下会有一个MyNote.exe,以及app和runtime两个子目录。双击exe,程序就跑起来了。

对大多数内部工具和临时交付场景,这种方式其实就够了——不需要安装,直接把整个目录压缩成一个zip发给对方,解压双击exe即用,完事。

但有个细节要注意:如果这时候程序包含多个jar,比如依赖了commons-io.jar、gson.jar,你要么手动把它们都放到target下,要么用Maven先打成fat jar。对于普通项目,我习惯直接用maven-assembly-plugin或者maven-shade-plugin打一个包含全部依赖的可执行jar,这样--main-jar只需要指定一个文件。

4.2 一键生成安装包:--type exe的完整参数

如果你要给客户交付一个“像样的安装包”,那就得用--type exe。命令变成这样:

jpackage --type exe --input target --main-jar my-note.jar --name MyNote --app-version 1.0.0 --vendor "MyCompany" --icon app.ico --win-menu --win-shortcut --win-dir-chooser --dest dist

这里多的几个参数:

  • --type exe:生成Windows安装程序;
  • --app-version 1.0.0:程序版本号,必须符合X.Y.Z格式,最好每次发版都递增;
  • --vendor "MyCompany":公司信息,会在安装向导以及“卸载程序”里显示;
  • --icon app.ico:exe图标,必须是Windows标准的.ico文件,不能直接用png;
  • --win-menu:在开始菜单创建快捷方式;
  • --win-shortcut:在桌面创建快捷方式;
  • --win-dir-chooser:安装时让用户自定义安装目录。

执行完后,dist目录下会生成一个类似MyNote-1.0.0.exe的安装程序。双击安装,完事在桌面和开始菜单都有入口。

关于WiX Toolset的坑:如果你直接执行--type exe,可能会遇到一个报错提示找不到WiX。这是因为jpackage生成exe安装包依赖WiX工具集,早期的JDK版本需要你手动安装WiX Toolset 3.x并配置到PATH。解决办法是去WiX官网下载WiX 3.14安装包,安装完成后把C:\Program Files (x86)\WiX Toolset v3.14\bin加入PATH再重新打包。另外,不要用WiX 4.x,jpackage对WiX 4支持不稳定,老老实实装3.x版本。如果你实在不想装WiX,那就用前面的--type app-image跑通流程,毕竟安装包只是分发形态之一。

4.3 验证打包结果

打包完成后,强烈建议你做两步验证,而不是直接把文件发出去。

第一步,在一台干净的、没有安装JDK的Windows机器(或者虚拟机)上跑exe,确认程序能正常启动和退出。如果没有干净的测试环境,至少把安装目录下的runtime文件夹删掉之后试试,如果程序还能跑,说明它动用了系统里的Java,这种环境下的测试没有意义。

第二步,检查安装目录下的文件结构。标准情况下是这样的:

C:\Program Files\MyNote\ ├── app\ │ ├── my-note.jar │ └── ...其他资源 ├── runtime\ │ ├── bin\java.exe │ └── ...大量JRE文件 └── MyNote.exe

如果你发现app目录下多了不该带出去的内部文件(比如数据库密码配置文件),记得调整--input目录的筛选逻辑。这块我吃过亏,曾经顺手把本地开发的application-dev.yml带进了正式包,那酸爽滋味不想再尝第二次。

5. 进阶操作:图标、版本信息与安装体验定制

默认情况下,jpackage打出来的exe是标准的Java图标,版本信息也很简略。想让你的程序看起来更像正经软件,下面这几个定制点值得花时间。

5.1 Windows图标那点事

图标是用户对软件的第一印象,Java默认那个咖啡杯图标实在不适合交付。Windows下的exe图标必须是.ico格式,不是普通的png改名就能冒充的。你可以用在线转换工具把png转成ico,或者用ImageMagick:

magick convert app.png -resize 256x256 app.ico

制作ico时注意这几个细节:

  • 推荐包含多尺寸图层,至少要有16、32、48、256这几个档位,避免你在桌面和任务栏看到模糊锯齿的图标;
  • 透明背景和圆角设计在Windows上表现良好;
  • 不要在图标里放小字号文字,Windows会在16x16尺寸下自动缩放,观感很差。

然后把生成的app.ico放到项目根目录,打包命令里加上--icon app.ico。如果你用第二步的app-image方式已经生成了exe,重新打包时会看到图标更新生效。

5.2 版本号与公司信息的正确姿势

--app-version、--vendor这两个参数直接影响“控制面板-程序和功能”里显示的详细信息。版本号一定按三段式写,比如1.2.3,不要写1.2这种两段式,Windows的文件属性会不认。

在Windows资源管理器里查看这个文件的“详细信息”标签页,你会看到:文件说明、产品名称、文件版本、产品版本、公司名称这些元数据。jpackage会根据--name、--app-version、--vendor自动填写。如果你需要更细的版权信息和控制,需要使用--win-update-url、--win-help-url这些附加参数,但普通场景下没必要折腾。

这里有个非常容易踩的坑:jpackage生成的exe安装包默认在“数字签名”一栏是空的,Windows会把它标记为“未知发布者”,SmartScreen可能会弹蓝屏警告。这不是你的程序有问题,是没买代码签名证书导致的。对个人和小团队来说,花几千块钱买证书不现实,临时处理方式是让用户在SmartScreen弹窗里点“更多信息-仍要运行”。如果客户比较敏感,建议在交付说明里写清楚这一步。

5.3 免安装版和安装版怎么取舍

app-image得到的是一个绿色免安装目录,exe包得到的是标准安装程序。两者各有利弊。

对比项app-image(绿色版)exe(安装版)
交付方式压缩包发过去解压即用跑一遍安装向导
快捷方式无,要用户自己建或手动放一个自动创建桌面/开始菜单快捷方式
卸载入口无控制面板标准卸载
用户心智适合极客和内部工具更像正式商业软件
升级体验直接替换文件夹可以覆盖安装

我的经验是:内部工具、开发辅助工具、给同行的东西,用app-image压缩成zip发过去就完事了,省去安装步骤。给非技术客户、正式对外发布的产品,用exe安装包。两者并不互斥,一条命令而已,所有参数都一样,只是--type不同,我一般两者都出,按交付对象选。

6. 给exe减肥:jlink定制运行时减少一半体积

用默认方式打出exe后,你可能会被它的体积吓到——一个最简单的Hello World程序,打包出来安装目录普遍在150MB到200MB之间。这正常吗?正常。因为jpackage默认会把一套完整的JRE运行时塞进runtime目录,而你的程序可能只用了整套JRE里不到10%的功能。

针对这个问题的标准解法是jlink。它能根据你程序实际用到的模块,裁剪出一个迷你JRE,把不要的东西统统扔掉。配合jpackage的--runtime-image参数,体积可以轻松压到60MB甚至40MB以下。

6.1 先搞清楚你的程序依赖了多少模块

在裁剪之前,得先知道程序到底用了哪些JDK模块。JDK提供了一个专门干这个的工具叫jdeps,它对jar包做依赖分析。

jdeps --print-module-deps target/my-note.jar

比如我的小记事本程序,输出结果是:

java.base,java.desktop,java.logging

这意味着我的程序只需要这三个模块,运行时里和大数据、网络编程、服务器相关的一堆模块都可以不打包进去。如果你的程序用了Swing/AWT,那java.desktop是必需的;如果用到了HTTP请求,可能需要java.net.http;用到了XML解析,则要加java.xml。具体以jdeps实际的输出为准。

6.2 用jlink生成精简运行时

拿到模块依赖列表后,执行:

jlink --add-modules java.base,java.desktop,java.logging --output runtime-image --strip-debug --no-man-pages --no-header-files --compress=2

参数解释:

  • --add-modules:指定要保留的模块,多个模块用英文逗号隔开;
  • --output runtime-image:生成的精简JRE目录;
  • --strip-debug:去掉调试符号,能省不少空间;
  • --no-man-pages、--no-header-files:去掉文档和头文件,生产环境没人看这些;
  • --compress=2:启用压缩,能进一步减小体积,某些JDK版本可能需要写--compress=zip-6,如果报错就换一下写法。

执行完成后,runtime-image目录大概在40MB到60MB之间,比你完整的JRE小了不止一倍。

6.3 把精简运行时交给jpackage

关键一步:把jlink生成的runtime-image作为运行时传给jpackage。

jpackage --type app-image --input target --main-jar my-note.jar --runtime-image runtime-image --name MyNote --dest dist

加了--runtime-image之后,jpackage会直接使用你提供的精简运行时,不再去你JDK目录里拷贝完整JRE。

6.4 瘦身前后的直观对比

我以一个小型的JavaFX记事本工具为例,实测数据如下:

打包方式安装目录体积备注
默认jpackage约185MB包含完整JRE
默认jpackage + 手动删JRE模块约90MB手动删容易删错
jlink裁剪后约55MB只保留3个模块+压缩
GraalVM原生编译约25MB启动快,但适配成本高

所以,如果你对分发体积有要求,jlink + jpackage是最值得掌握的黄金组合。它不要求你修改源代码,只是加了一道聪明的裁剪,却能把150MB+的包变成50MB左右,这对网络传输和用户下载体验是质的改善。

有一点需要提醒:jlink裁剪后,运行时里可能没有你项目里动态用到的类,比如反射、SPI机制加载的类。这类动态引用危机如果程序启动时调用了不存在的类,会报NoClassDefFoundError或ClassNotFoundException。出现这种情况,要么用--bind-services参数保留ServiceLoader相关服务,要么干脆把可能导致问题的模块也加进--add-modules里。实践上,多留一两个模块的代价只有几MB,远比排查动态加载问题划算。

7. 现场翻车记录:打包过程中最常见的六类问题

这一节是我花了最多精力整理的内容。打包命令本身很简单,真正让人抓狂的是各种“双击exe没反应”“闪退”“杀毒软件报毒”这些现场问题。每条都是我或者朋友真实踩过、一晚上一晚上排查出来的教训。

7.1 双击没反应:一闪而过怎么排查

双击exe,屏幕闪了一下,程序没起来。这是最经典的问题,几乎每个打包新手都会遇到。原因通常有两个。

第一个原因是程序启动时抛了异常,但异常信息被Windows吞掉了。解决办法是给jpackage命令加上--win-console参数,生成一个带控制台窗口的调试版exe:

jpackage --type app-image --input target --main-jar my-note.jar --win-console --name MyNoteDebug --dest dist

双击这个带控制台窗口的exe,异常堆栈就会直接打在命令窗口里,问题一目了然。排查完,去掉--win-console重新打包即可。

第二个原因是主类没找到。如果你的jar的MANIFEST.MF里没有Main-Class,或者你用了--main-class但类名写错了,程序也会起不来。用命令检查jar包:

jar xf my-note.jar META-INF/MANIFEST.MF type META-INF\MANIFEST.MF

确认Main-Class这一行是不是你的启动类全限定名。注意,Spring Boot fat jar这里是org.springframework.boot.loader.JarLauncher,不是你的业务启动类,不要改。

7.2 杀毒软件把exe当木马:SmartScreen与误报处理

jpackage生成的exe没有数字签名,在Windows 10/11上很容易触发SmartScreen“Windows已保护你的电脑”这个蓝色弹窗。更闹心的是,有些杀毒软件会直接把未签名的打包程序标注为“高风险”。这几乎是所有Java打包工具的通病,连GraalVM原生编译也逃不过。

短期对策是在打包说明里告诉用户:这个程序没有签名,所以SmartScreen会弹窗,点“更多信息-仍要运行”即可。长期对策是买代码签名证书,用signtool对exe做数字签名,但证书一年几百到几千不等,个人开发者通常不划算。

还有一条容易被忽视的:如果你在代码里用了下载、解压、修改注册表这类行为,杀毒软件误报的概率会直线上升。打包前可以把这些敏感操作降噪一下,比如不要用运行时下载jar然后反射加载的方式,这种“下载可执行内容再执行”的模式极度容易触发本地安全软件的启发式引擎。

7.3 中文路径和空格路径的坑

jpackage在路径处理上对中文和空格有兼容性,但你的程序内部不一定兼容。比如程序里如果写死了某个相对路径,或者用了File处理资源文件,在C:\Program Files\MyNote\这种带空格的路径下,就有可能出问题。

我在实践中养成了一个习惯:程序内部需要定位文件时,永远不要用相对路径,也不要硬编码C:\...,而是通过System.getProperty("user.dir")、或者从jar本身的代码源推导出所在目录。更稳妥的方案是把资源打进jar包,用ClassLoader.getResourceAsStream读取。

打包时还要注意,--input指定的目录路径,以及--dest输出的路径,不要带中文和特殊字符。比如D:\项目打包\目标文件这种,很可能触发WiX或jpackage的路径编码问题,报各种莫名其妙的错。全项目统一用英文路径,能帮你省掉大量烦恼。

7.4 fat jar与依赖冲突

如果你用maven-shade-plugin打fat jar,然后把所有依赖打进一个jar里,jpackage处理起来最常见的问题是某些依赖jar里的META-INF目录和服务描述文件冲突,比如多个jar都有META-INF/services/javax.xml.parsers.SAXParserFactory。这种冲突在常规运行时可能不明显,但jpackage的模块化分析阶段容易炸。

一个有效习惯是:用maven-shade-plugin时配置一下ServicesResourceTransformer,让SPI服务文件合并而不是互相覆盖:

<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>

如果你不想折腾shade插件,改用maven-assembly-plugin打出的fat jar结构更简单,针对jpackage的兼容性也更好。多依赖场景下,我的选择顺序是:Spring Boot用自带的repackage,普通项目用Assembly,最后才考虑Shade。

7.5 数据文件、配置文件放哪里

很多程序都会使用外部的配置文件、SQLite数据库文件、日志目录等。这些文件如果你直接放在jar包外面,打包时就得考虑它们去哪里。

正确做法是,把配置文件作为资源放进jar包,或者放到--input目录下,这样jpackage会把它们一并拷贝到安装目录的app子目录。在程序里读取时,不要假定当前工作目录是exe所在目录,Windows的快捷方式启动和命令行启动,工作目录不一定相同。

推荐这种读法:

Path configPath = Paths.get(System.getProperty("user.dir"), "config", "app.properties");

但更稳妥的是,参照exe所在目录来定位:

String exePath = System.getProperty("jpackage.app-path");

jpackage在启动时会注入一个jpackage.app-path系统属性,指向exe文件本身,这在官方未写明但在实践中可用。拿到exe路径后,Paths.get(exePath).getParent()就是程序所在目录,再拼上app子目录,就能找到jpackage拷贝进去的资源。这个东西很实用,但网上资料少,我踩了好几次坑才总结出来。

7.6 日志和错误输出怎么抓

打包后的程序和开发环境不同,控制台看不到日志。排查问题时,我习惯给程序加一个简单的日志机制,至少把启动异常写到文件:

try { // 启动逻辑 } catch (Throwable t) { Files.write(Paths.get("error.log"), Arrays.toString(t.getStackTrace()).getBytes()); throw t; }

更标准的做法是引入slf4j+logback,配置logback.xml,让日志输出到安装目录下的logs文件夹。但注意,运行时可能没有对安装目录的写入权限,特别是安装到C:\Program Files下时。遇到这种情况,日志目录可以设计成用户目录下的%LOCALAPPDATA%\MyNote\logs,既保证可写,又符合Windows约定。

8. 固化为脚本:一条命令完成整个打包流程

手动敲jpackage命令是学习阶段的事,真正工作流里,你肯定不想每次发版都重新敲一遍那串长参数,更不希望团队成员各自用各自的参数打包,最后产出一堆版本混乱的包。把打包流程固化成脚本和配置文件,是专业团队的底线操作。

8.1 Windows批处理一键打包

最简单的做法是写一个build.bat,把前面的所有步骤串起来:

@echo off setlocal set APP_NAME=MyNote set APP_VERSION=1.0.0 set MAIN_JAR=my-note.jar set MODULES=java.base,java.desktop,java.logging echo [1/4] Maven package... call mvn clean package echo [2/4] jdeps analyze... call jdeps --print-module-deps target\%MAIN_JAR% >modules.txt set /p MODULES=<modules.txt echo [3/4] jlink runtime... call jlink --add-modules %MODULES% --output runtime-image --strip-debug --no-man-pages --no-header-files --compress=2 echo [4/4] jpackage exe... call jpackage --type exe --input target --main-jar %MAIN_JAR% --runtime-image runtime-image --name %APP_NAME% --app-version %APP_VERSION% --vendor "MyCompany" --icon app.ico --win-menu --win-shortcut --win-dir-chooser --dest dist echo Done. endlocal

这个脚本里,jdeps的结果会动态读取,如果你新增了网络、XML等功能,模块列表会自动更新,不需要你手动改MODULES变量。注意用set /p读取文件时不要带空格和换行符的问题,如果发现模块串行,可以考虑用循环方式读取。

8.2 把打包链接进Maven生命周期

还有人希望连脚本都不用敲,直接mvn package就把exe一起打出来。这个需求可以用exec-maven-plugin在package阶段后调用jpackage命令。这样你在CI/CD流水线上也会非常舒服,一次构建,jar和exe同时产出。

在pom.xml里加:

<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.1.0</version> <executions> <execution> <id>jpackage</id> <phase>package</phase> <goals> <goal>exec</goal> </goals> <configuration> <executable>jpackage</executable> <arguments> <argument>--type</argument><argument>exe</argument> <argument>--input</argument><argument>target</argument> <argument>--main-jar</argument><argument>${project.build.finalName}.jar</argument> <argument>--runtime-image</argument><argument>runtime-image</argument> <argument>--name</argument><argument>${project.artifactId}</argument> <argument>--app-version</argument><argument>${project.version}</argument> <argument>--dest</argument><argument>dist</argument> </arguments> </configuration> </execution> </executions> </plugin>

注意,用Maven的${project.version}直接作为--app-version时必须符合X.Y.Z格式,如果你的Maven版本号是1.0-SNAPSHOT这种,jpackage会直接报错。有SNAPSHOT版本需求的话,要么在包版本里改成1.0.0-SNAPSHOT,要么在插件参数里去掉--app-version。

8.3 团队协作时的产物管理与版本规范

最后聊一点流程层面的经验。打包这件事一旦进入团队协作,就不再是技术问题了,而是规范和产物管理问题。我个人实践下来这几条特别有用:

  • 约定产物命名规则,比如MyNote-1.0.0-x64.exe,包含应用名、版本号,有需要再加平台和架构,避免别人在群里问“这个exe是哪个版本”;
  • 维护一个简单的CHANGELOG,每次发版记录新增功能和修复的问题,放进安装目录或项目里,对技术支持的帮助巨大;
  • 每次打包最好从干净的tag或release分支构建,不要用本地改动一堆的分支打“测试包”,否则发出去的东西和代码对不上,排查问题会怀疑人生;
  • 包发布后保存好对应的jar和源码tag,这样用户反馈问题时,你还能反编译jar对照当时的代码。谈到反编译,不得不提醒一句:jar包本质上是明文交付,别人用反编译工具很容易看到你的源码逻辑甚至硬编码的密钥。exe化之后虽然多了层壳,但jar本体还在安装目录里。如果你要做商业软件,建议至少做一层代码混淆,加密硬编码密码和其他敏感信息。

说实话,每次把一堆命令行参数写进脚本的时候,都会想起十年前那个被客户逼着研究“jar变exe”的晚上。工具链从Launch4j到exe4j再到官方jpackage,这条路越来越顺,但核心逻辑没变过:你要交付的不是代码,是一个用户能轻松打开并愉快使用的程序。我在实际项目里最深的体会是,打包这件事看起来是收尾,但如果你前面的工程规范没做好,它会反咬你一口;反过来,当你把打包流程固化好,自动跑上,后面每次发版都能省出半天时间,这种“一次性投入,长期收益”的事情,早做早安心。希望这篇整理能让你少走点弯路,更多的人不再为“如何给Java程序做壳子”这种基础而折磨人的事情焦虑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询