☰
Java桌面工具打包:exe4j结合jlink精简JRE实战
2026/10/13 6:24:06 网站建设 项目流程

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、jpackage(JDK 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 被正确加入 classpath,main 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.desktop(Swing/AWT)、java.logging(日志)、java.sql(数据库)、java.naming(JNDI,某些框架会用到)、jdk.crypto.ec(HTTPS 相关加密)。

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 ^ --compress=2

逐个参数解释一下,这些参数直接决定最终体积:

  • --module-path:指向 JDK 安装目录下的 jmods 文件夹,里面是各模块的打包文件。这个路径必须对,否则 jlink 找不到模块。
  • --add-modules:要包含的模块列表,逗号分隔,不能有空格。
  • --output:输出目录名,不能是已存在的目录,jlink 不会覆盖。
  • --strip-debug:去掉调试信息,能省几 MB。
  • --no-header-files:不包含 C 头文件,桌面程序用不到。
  • --no-man-pages:不包含手册页,同样用不到。
  • --compress=2:压缩级别,2 是最高压缩。注意在较新的 JDK 里这个参数写法有变化,JDK 21 之后改成了--compress=zip-6这种形式,具体看你的 JDK 版本。

实测一组体积数据供参考(基于 JDK 17,64 位 Windows):

模块组合输出体积
java.base约 42 MB
java.base + java.desktop约 55 MB
java.base + java.desktop + java.logging约 57 MB
java.base + java.desktop + java.sql + java.naming约 68 MB
完整 JRE约 190 MB

可以看到,即使加上几个常用模块,体积也远小于完整 JRE。--compress=2之后还能再小一些,但启动时会有轻微的解压开销,小工具基本感知不到。

注意:jlink 生成的运行时是平台相关的。在 Windows 上用 Windows 的 JDK 生成,产物只能在 Windows 上跑。如果要跨平台,得在对应平台上分别生成。

4. 把精简 JRE 接进 exe4j 并验证的完整过程

4.1 目录结构怎么摆

精简 JRE 生成好之后,目录结构建议这样组织:

your-app/ ├── your-app.exe ├── custom-jre/ │ ├── bin/ │ ├── lib/ │ └── ... └── lib/ └── your-app.jar

exe 和 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.sql,JSON 库加java.desktop(某些实现会用到);二是在测试阶段把所有功能路径都走一遍,别只测启动。我遇到过启动正常、点某个按钮才崩的情况,就是那个按钮触发的代码路径用到了没加的模块。

5.2 压缩参数在不同 JDK 版本里写法不一样

前面提过,--compress=2在 JDK 21 之后改成了--compress=zip-6这种形式。如果你照着老教程写--compress=2,在新 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,环境变量会掩盖问题。找一台干净机器或者虚拟机做最终验证,这一步不能省。

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

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

立即咨询