Java桌面工具打包:exe4j结合jlink精简JRE实战

📅 发布时间:2026/10/13 6:24:21
Java桌面工具打包:exe4j结合jlink精简JRE实战
1. 从 jar 到 exe 再到精简 JRE一个被低估的交付环节做过 Java 桌面工具交付的人大概都有过这种体验功能开发完了打包成 jar 双击能跑但一旦要发给非技术同事或者客户问题就来了——对方机器上没装 Java或者装的是个版本对不上的 JRE双击没反应弹个黑框一闪而过。这时候最直接的做法就是用 exe4j 把 jar 包成 exe再配一个 JRE 一起发过去。但紧接着第二个问题冒出来完整 JRE 动辄一百多兆甚至两百兆一个几十 KB 的小工具配一个两百兆的运行环境压缩包发出去对方都嫌大。这篇内容就是围绕这个链路来的用 exe4j 把 jar 转成 exe然后对随附的 JRE 做精简把体积压到最小可用状态。整套流程不涉及复杂的构建工具链也不需要写脚本做模块裁剪核心思路就一句话——用 jlink 生成一个只包含你项目实际用到模块的定制运行时再把它交给 exe4j 引用。这是目前在不引入额外打包框架比如 jpackage的前提下最简单、最可控的精简方式。适合谁看如果你正在做 Java 桌面小工具、内部辅助程序、教学演示软件需要交付一个双击就能跑、不依赖用户装 Java的绿色包那这套流程基本可以直接抄。如果你已经在用 exe4j 但一直被体积问题困扰那第 3 节和第 4 节会是你最关心的部分。整篇内容基于实际项目反复验证过的步骤参数和命令都可以直接复现。2. exe4j 打包 jar 的完整操作链路与关键配置2.1 为什么选 exe4j 而不是其他方案在动手之前先把选型逻辑说清楚不然后面精简 JRE 的时候容易走弯路。Java 桌面程序转 exe常见路子有这么几条exe4j、jpackageJDK 14 自带、Launch4j、GraalVM native-image。它们各自的定位差别很大。exe4j 的优势在于对 JRE 的引用方式非常灵活——它允许你在配置里指定一个相对路径的 JRE 目录程序启动时用这个目录里的 java 可执行文件来加载你的 jar。这个特性是后面能做 JRE 精简的前提。jpackage 虽然也能打包运行时但它会把运行时嵌进安装目录定制粒度不如手动控制 JRE 目录来得直观。Launch4j 更偏向轻量包装对 JRE 的处理相对简单。GraalVM native-image 是另一条路直接编译成原生二进制但那套东西对反射、动态代理的支持有坑小工具未必值得折腾。所以结论是如果你的目标是jar 可控 JRE 目录 exe 启动器这种结构exe4j 是最顺手的。它不追求把一切塞进一个文件而是给你一个清晰的目录结构方便你后续替换和精简运行时。2.2 exe4j 配置中几个容易配错的字段exe4j 的向导界面步骤不少但真正影响运行结果的字段集中在几个地方。我按实际踩过的顺序说。第一处是 Executable name 和 Output directory。输出目录建议单独建一个干净的文件夹因为 exe4j 生成的不只是 exe还会带一些辅助文件。如果输出目录里本来就有旧版本文件容易混淆。第二处是 Java invocation 里的 classpath 和 main class。这里要确保你的 jar 被正确加入 classpathmain class 写全限定名。如果项目依赖了第三方 jar全部加进来顺序按依赖关系排好。实测下来classpath 里条目过多时exe4j 的界面操作会比较繁琐建议提前把依赖整理成一个 lib 目录用通配符或者批量添加。第三处也是最关键的是 JRE 配置页。这里有几个选项Search sequence、Bundled JRE、Minimum/Maximum Java version。做精简 JRE 的时候要选Bundled JRE然后把路径指向你后面用 jlink 生成的那个定制运行时目录。注意路径最好用相对路径相对于 exe 所在目录这样整个文件夹拷到别的机器上也能跑。第四处是 Minimum Java version。这个值要和你定制运行时里的 Java 版本一致。比如你用 JDK 17 的 jlink 生成运行时这里就填 17。填高了启动直接报错填低了虽然能过但没意义。提示exe4j 在 32 位和 64 位 JRE 的处理上有区别。如果你的定制运行时是 64 位的exe4j 生成的启动器也要对应 64 位模式否则会出现无法加载 JVM的错误。这个在向导的 JRE 页里有位数相关的选项别漏看。2.3 打包完成后先别急着精简先验证很多人一打包完就直奔精简结果精简完跑不起来分不清是 exe4j 配置问题还是 JRE 裁剪问题。正确的顺序是先用一个完整的 JRE 跑通整个 exe确认功能正常再替换成精简版。具体做法把完整 JRE 目录放到 exe 同级exe4j 里指向它双击 exe确认程序能正常启动、功能正常、退出正常。这一步过了说明 exe4j 的配置没问题后面精简出的任何问题都能定位到 JRE 本身。这个先全量后精简的顺序能省掉大量来回排查的时间。3. jlink 定制运行时精简 JRE 的核心手段3.1 jlink 到底做了什么jlink 是 JDK 9 引入的模块化工具它的作用是根据你指定的模块集合组装出一个只包含这些模块及其依赖的运行时镜像。打个比方完整 JRE 像是一个装满所有工具的仓库而 jlink 允许你只搬走你实际要用的那几件工具外加它们必须的配件。Java 9 之后JDK 本身被拆成了一个个模块module每个模块有明确的依赖声明。jlink 读取这些声明自动把依赖闭包算出来然后从 JDK 里抽取对应的类文件和本地库拼成一个独立的运行时目录。这个目录里有 bin/java、bin/javaw、lib 等结构和完整 JRE 一样但体积小得多。一个典型的桌面小工具如果只用到基础类库和 Swing定制出来的运行时可以压到 30 到 50 MB 左右相比完整 JRE 的两百兆缩减非常明显。如果项目还用了数据库驱动、网络库体积会相应增加但通常也能控制在 60 MB 以内。3.2 怎么确定项目需要哪些模块这是整个流程里最需要动脑的一步。模块列少了运行时报 ClassNotFoundException 或 NoClassDefFoundError列多了体积压不下去。有两种确定方式。第一种是查 jdeps。JDK 自带 jdeps 工具可以分析你的 jar 依赖了哪些模块。命令大致是这样jdeps --multi-release 17 --ignore-missing-deps --print-module-deps your-app.jar输出会是一串模块名比如java.base,java.desktop,java.logging。这串结果可以直接喂给 jlink 的--add-modules参数。注意--ignore-missing-deps是为了跳过那些找不到的第三方依赖避免分析中断。第二种是手动试错。先给一个最小集合比如java.base跑起来看报什么错缺什么补什么。这种方式适合依赖关系简单的小工具但对稍微复杂点的项目效率低。实际项目里我一般以 jdeps 的结果为起点然后手动补几个常见模块。因为 jdeps 对反射、动态加载的代码分析不出来这类代码用到的模块它不会报。常见的需要手动补的有java.desktopSwing/AWT、java.logging日志、java.sql数据库、java.namingJNDI某些框架会用到、jdk.crypto.ecHTTPS 相关加密。3.3 jlink 命令的完整参数与实测体积对照一条典型的 jlink 命令长这样jlink --module-path C:\Program Files\Java\jdk-17\jmods ^ --add-modules java.base,java.desktop,java.logging ^ --output custom-jre ^ --strip-debug ^ --no-header-files ^ --no-man-pages ^ --compress2逐个参数解释一下这些参数直接决定最终体积--module-path指向 JDK 安装目录下的 jmods 文件夹里面是各模块的打包文件。这个路径必须对否则 jlink 找不到模块。--add-modules要包含的模块列表逗号分隔不能有空格。--output输出目录名不能是已存在的目录jlink 不会覆盖。--strip-debug去掉调试信息能省几 MB。--no-header-files不包含 C 头文件桌面程序用不到。--no-man-pages不包含手册页同样用不到。--compress2压缩级别2 是最高压缩。注意在较新的 JDK 里这个参数写法有变化JDK 21 之后改成了--compresszip-6这种形式具体看你的 JDK 版本。实测一组体积数据供参考基于 JDK 1764 位 Windows模块组合输出体积java.base约 42 MBjava.base java.desktop约 55 MBjava.base java.desktop java.logging约 57 MBjava.base java.desktop java.sql java.naming约 68 MB完整 JRE约 190 MB可以看到即使加上几个常用模块体积也远小于完整 JRE。--compress2之后还能再小一些但启动时会有轻微的解压开销小工具基本感知不到。注意jlink 生成的运行时是平台相关的。在 Windows 上用 Windows 的 JDK 生成产物只能在 Windows 上跑。如果要跨平台得在对应平台上分别生成。4. 把精简 JRE 接进 exe4j 并验证的完整过程4.1 目录结构怎么摆精简 JRE 生成好之后目录结构建议这样组织your-app/ ├── your-app.exe ├── custom-jre/ │ ├── bin/ │ ├── lib/ │ └── ... └── lib/ └── your-app.jarexe 和 custom-jre 同级jar 放在 lib 下。exe4j 里 JRE 路径填custom-jre相对路径classpath 里引用lib/your-app.jar。这样整个 your-app 文件夹拷到任何 Windows 机器上都能直接跑不需要目标机器装 Java。4.2 替换后启动失败的几种典型表现与排查替换成精简 JRE 后最常见的失败有这么几类我按遇到频率排一下。第一类启动瞬间闪退没有任何提示。这种多半是 JRE 路径没配对或者 exe4j 里 JRE 位数和实际不符。排查方法是在命令行里运行 exe这样能看到错误输出。如果提示找不到 jvm.dll就是路径或位数问题。第二类报 NoClassDefFoundError指向某个具体类。这是模块缺失的典型信号。看报错里的类属于哪个包反查它属于哪个模块补进--add-modules重新生成。比如报javax/sql/DataSource找不到就是缺java.sql。第三类报加密相关异常比如 SSL 握手失败。这种是缺jdk.crypto.ec模块。很多网络请求会用到椭圆曲线加密这个模块不在 java.base 里需要显式加。第四类中文乱码或者字体异常。精简 JRE 默认可能不带完整的字体配置。如果程序界面有中文显示问题检查lib/fonts目录必要时从完整 JRE 里拷对应的字体配置文件过来。排查的核心思路是命令行运行看报错根据报错反查模块补模块重新生成。一般来回两三次就能收敛到最小可用集合。4.3 一个反复验证过的模块清单模板基于多个桌面小工具的实践下面这个模块组合覆盖了大多数场景可以直接作为起点java.base,java.desktop,java.logging,java.sql,java.naming,java.xml,jdk.crypto.ec如果你的工具还涉及 HTTP 请求加上java.net.http涉及 Preferences 存储加上java.prefs涉及压缩解压加上java.compiler一般不需要但java.base里已含基础压缩。这个清单不是最优解但是一个大概率能跑起来的稳妥起点之后再根据实际报错做减法。5. 精简过程中那些文档不会写的坑5.1 反射和动态代理是模块分析的盲区jdeps 分析的是字节码里显式的类型引用但反射调用Class.forName、Method.invoke和动态代理在字节码层面看不出目标类。很多框架、序列化库、JDBC 驱动都大量用反射。这意味着 jdeps 给出的模块列表往往是不完整的。应对办法有两个一是对已知会用反射的库提前把相关模块加进去比如数据库驱动加java.sqlJSON 库加java.desktop某些实现会用到二是在测试阶段把所有功能路径都走一遍别只测启动。我遇到过启动正常、点某个按钮才崩的情况就是那个按钮触发的代码路径用到了没加的模块。5.2 压缩参数在不同 JDK 版本里写法不一样前面提过--compress2在 JDK 21 之后改成了--compresszip-6这种形式。如果你照着老教程写--compress2在新 JDK 上会直接报参数错误。这个坑很隐蔽因为报错信息不一定直白。建议先jlink --help看一眼当前版本的参数写法再动手。5.3 精简 JRE 里的 java.exe 和 javaw.exe 都要保留exe4j 生成的启动器在启动时根据配置可能调用java.exe或javaw.exe。javaw是不带控制台窗口的版本桌面程序一般用它。精简的时候这两个都要在bin目录里少一个都可能导致启动方式不匹配。jlink 默认会生成这两个一般不用手动处理但如果你手动删过文件记得检查。5.4 别把 jlink 生成的运行时和完整 JRE 混用文件有人为了省事从完整 JRE 里拷几个文件到精简 JRE 里补缺失的功能。这种做法短期能跑但容易引入版本不一致的隐患比如lib/modules文件和bin下的可执行文件版本对不上。正确做法是回到--add-modules补模块重新生成而不是手工拼文件。5.5 杀毒软件误报与签名问题exe4j 生成的 exe 有时候会被杀毒软件误报尤其是没有数字签名的。这个和 JRE 精简无关但交付时会遇到。如果目标环境有严格的安全策略可能需要走签名流程。精简 JRE 本身不会加剧这个问题但如果 exe 体积异常小某些启发式扫描反而更容易误判这点心里有数就行。6. 关于体积、启动速度与兼容性的取舍经验精简 JRE 不是越小越好这里有个平衡。模块砍得越狠体积越小但运行时缺模块的风险越高而且某些模块缺失导致的报错不一定在启动时暴露可能藏在某个冷门功能里。我的经验是在功能完整验证通过的前提下再考虑进一步做减法而不是一上来就追求极限体积。启动速度方面精简 JRE 通常比完整 JRE 启动略快因为要加载的类少了。但这个差异在小工具上不明显用户基本感知不到。真正影响启动速度的是 exe4j 的启动器配置和 jar 本身的初始化逻辑JRE 体积不是主要因素。兼容性方面jlink 生成的运行时和生成它的 JDK 版本强绑定。用 JDK 17 生成的运行时里面就是 17 的类库。如果你的代码用了更高版本 JDK 的特性那生成运行时的 JDK 版本必须匹配。跨大版本使用会出问题这点在团队协作时要统一好 JDK 版本。还有一个实际交付时的细节整个 your-app 文件夹建议打成 zip 再发因为里面文件多直接发文件夹容易在传输过程中丢文件或者被邮件系统拦截。zip 里保持目录结构不变对方解压即可用。如果体积还是偏大可以对 zip 本身再做一次压缩但收益有限因为 jlink 生成的运行时里很多文件已经是压缩过的了。最后说一个我反复验证过的判断标准当你把精简 JRE 接进 exe4j 后在一台从没装过 Java 的干净机器上把整个文件夹拷过去双击 exe 能正常启动并完成核心功能这个精简结果才算合格。开发机上跑通不算数因为开发机往往装了完整 JDK环境变量会掩盖问题。找一台干净机器或者虚拟机做最终验证这一步不能省。