
十年前我第一次给客户交付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“套壳”而是用AOTAhead-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为基线来讲。如果你明确知道目标机器上已经装了JDKLaunch4j也可以作为临时方案用。但如果目标是做成一个精致的商业软件且预算充足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 finalNamemy-note/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.4.1/version configuration archive manifest mainClasscom.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-imageapp-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.icoexe图标必须是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.xjpackage对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或者用ImageMagickmagick 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 --compress2参数解释--add-modules指定要保留的模块多个模块用英文逗号隔开--output runtime-image生成的精简JRE目录--strip-debug去掉调试符号能省不少空间--no-man-pages、--no-header-files去掉文档和头文件生产环境没人看这些--compress2启用压缩能进一步减小体积某些JDK版本可能需要写--compresszip-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参数生成一个带控制台窗口的调试版exejpackage --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 implementationorg.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; }更标准的做法是引入slf4jlogback配置logback.xml让日志输出到安装目录下的logs文件夹。但注意运行时可能没有对安装目录的写入权限特别是安装到C:\Program Files下时。遇到这种情况日志目录可以设计成用户目录下的%LOCALAPPDATA%\MyNote\logs既保证可写又符合Windows约定。8. 固化为脚本一条命令完成整个打包流程手动敲jpackage命令是学习阶段的事真正工作流里你肯定不想每次发版都重新敲一遍那串长参数更不希望团队成员各自用各自的参数打包最后产出一堆版本混乱的包。把打包流程固化成脚本和配置文件是专业团队的底线操作。8.1 Windows批处理一键打包最简单的做法是写一个build.bat把前面的所有步骤串起来echo off setlocal set APP_NAMEMyNote set APP_VERSION1.0.0 set MAIN_JARmy-note.jar set MODULESjava.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 MODULESmodules.txt echo [3/4] jlink runtime... call jlink --add-modules %MODULES% --output runtime-image --strip-debug --no-man-pages --no-header-files --compress2 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 groupIdorg.codehaus.mojo/groupId artifactIdexec-maven-plugin/artifactId version3.1.0/version executions execution idjpackage/id phasepackage/phase goals goalexec/goal /goals configuration executablejpackage/executable arguments argument--type/argumentargumentexe/argument argument--input/argumentargumenttarget/argument argument--main-jar/argumentargument${project.build.finalName}.jar/argument argument--runtime-image/argumentargumentruntime-image/argument argument--name/argumentargument${project.artifactId}/argument argument--app-version/argumentargument${project.version}/argument argument--dest/argumentargumentdist/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程序做壳子”这种基础而折磨人的事情焦虑。