恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
exe4j + jlink:无JDK环境下的Java应用打包与私有JRE裁剪
首页
资讯中心
/
exe4j + jlink:无JDK环境下的Java应用打包与私有JRE裁剪
exe4j + jlink:无JDK环境下的Java应用打包与私有JRE裁剪
发布时间:2026/9/23 21:17:05
简介面向 Java 开发者的实用教程重点讲解借助 exe4j 将 Java 工程打包成可执行 exe 文件使目标机器无需安装 JDK 也能直接运行适合需要分发桌面应用或简化部署流程的开发人员。压缩包内为单份 docx 文档大小约 925KB以手把手图文步骤从 Eclipse 导出 jar 包讲起逐步覆盖 exe4j 安装配置、JAR EXE 模式选择、依赖 jar 导入以及配置精简版 JRE 路径来摆脱对 JDK 的依赖完整还原 jar 转 exe 的流程。针对生成后可执行文件一闪而过的问题教程专门给出在 main 方法末尾加入 Thread.sleep(5000000) 保持窗口停留等排错思路帮助读者减少发布阶段的反复调试。目前已有 1253 人学习下载适合初到中级 Java 开发者参考按步骤操作即可独立完成打包发布尤其适合向客户交付无需预装 Java 环境的桌面程序。1. 用exe4j给Java工程再加一层启动器脱离JDK的机器也能直出执行文件很多 Java 开发者第一次听到 exe4j 时会下意识认为它能把 .class 编译成 .exe。实际上 exe4j 干的是另外一件事它生成一个原生 Windows 启动器这个启动器在当前机器上定位一整套 JVM然后把你的 main 方法交给 JVM 去执行。换句话说问题不在于Java 能不能编译成 exe而在于exe 怎么帮你把一个 JVM 抬起来。当目标机器没有安装 JDK 时只要 exe4j 按搜索顺序找到私有 JRE应用照样运行这就绕开了客户机必须预装 Java 环境的分发困境。适合做企业工具、运维脚本、桌面小工具交付的开发者阅读里面的 JVM 搜索顺序、版本对齐和 jlink 裁剪知识对五六年经验的后端也值得再翻一遍。2. 先弄懂 JVM 查找顺序再决定捆绑 JRE 还是复用系统 JRE2.1 exe4j 的启动器是找 JVM 并拉起 JVM不是把字节码编译成本地代码exe4j 生成的 .exe 是一个用 C/C 编写的 PE 格式启动器。它启动后执行的操作序列大致是这样先读自身携带的配置然后按照你设定的搜索顺序去寻找 jvm.dll找到之后通过 JNI 调用JNI_CreateJavaVM创建 JVM 实例再把 classpath、系统属性、main 类名传进去最后调用 main 方法。整个过程里没有字节码到机器码的编译步骤Java 代码仍然运行在 JVM 上exe4j 只负责把 JVM 这个运行时正确地抬起来。因此未安装 JDK 的机器并不是要求你在客户机上静默安装一套完整 JDK而是要求这台机器上存在一个可被启动器发现的 JRE。这里面有个容易被忽略的差异客户机可能装了其他软件自带的 JRE例如某些中间件会注册自己的 Java 运行时但它可能版本太老、位数不对或路径带空格。所以 exe4j 的分发策略通常不是依赖运气而是让应用自带 JRE通过搜索顺序让私有运行时成为首选。2.2 JRE 搜索序列为什么相对路径放在第一位最稳exe4j 在向导中的 Search sequence 选项就是一段按优先级排列的候选位置。一个典型的配置如下优先级搜索位置表达方式适用场景1exe 同级的私有 JREruntime\win64-jre推荐目录随 U 盘走目标机器无 Java2环境变量 JAVA_HOME%JAVA_HOME%开发机调试或公司统一预装 JDK3Windows 注册表registry: HKLM\SOFTWARE\JavaSoft\JRE系统安装了 JRE4公共安装目录C:\Program Files\Common Files\Oracle\Java\javapath依赖系统层 Java 的旧方案把私有 JRE 放在第一位能保证即使客户机环境变量配置得乱七八糟也不会被外面的 JDK 干扰。搜索顺序里相对路径的解析基准是 exe4j 启动器所在目录不是启动时的当前工作目录所以把整个应用目录从 D 盘挪到 C 盘搜索位置仍然有效。开发时把后备项留着的目的是本地调试能快速切回系统 JDK但生产发布建议删掉这些后备项否则目标机正好有一个版本不兼容的系统 JRE启动器可能跳过私有 JRE 而去用它给排错增加难度。2.3 用 jlink 造私有 JRE裁剪一个能放进应用目录的运行时私有 JRE 最省心的来源是在 Windows 开发机上用 Windows 版 JDK 里的 jlink 命令生成。jlink 是 JDK 9 就有的模块化工具它按--add-modules指定的模块清单把完整运行时裁剪成一个独立目录。建议先从一个可靠的开源镜像站或 JDK 官网下载 Windows 版 JDK 17 压缩包解压后执行set JAVA_HOMED:\jdk-17 %JAVA_HOME%\bin\jlink ^ --module-path %JAVA_HOME%\jmods ^ --add-modules java.base,java.desktop,java.logging,java.management,java.naming ^ --strip-debug --no-header-files --compress2 ^ --output D:\app\runtime\win64-jre这段命令里--module-path指向 JDK 自带的 jmods 目录--add-modules列出应用直接或间接用到的模块--strip-debug去掉调试符号--no-header-files不保留 native 头文件--compress2对模块镜像做压缩。最终生成的win64-jre目录里有bin\server\jvm.dll和release文件exe4j 搜索时靠它们判断这到底是不是一个可用的 JVM。裁剪模块表不完整会出问题特别是反射、SPI 和动态代理调到的模块编译期看不出依赖。更稳的做法是用 jdeps 自动分析 jar 的模块依赖%JAVA_HOME%\bin\jdeps --print-module-deps D:\app\app.jar把输出内容填进--add-modules再补上框架需要的java.sql、java.naming等模块。如果运行 jlink 时提示模块缺失通常不是 jlink 的问题而是 jdeps 输出里漏了反射调用的目标手工补上一个模块后重跑即可。注意 jlink 必须在 Windows 平台的 JDK 上执行在 Linux 里生成出来的运行时不能直接给 Windows 的 exe4j 用。2.4 捆绑哪些东西取决于你的分发预算这里要把三种常见路线放一起比捆绑裁剪 JRE、捆绑完整 JRE、完全不捆绑只依赖系统 JRE。方案额外体积客户机条件维护成本裁剪 JRE exe4j40~90MB无任何要求升级 JDK 时重新裁剪完整 JRE exe4j200~300MB无任何要求拷贝简单但升级一致只依赖系统 JRE0客户机必须自己装 JDK/JRE环境版本不稳定我一般会选裁剪 JRE体积和可控性平衡最好。很多五年以上经验的开发者还在用第二条路线整包 300MB 发给客户传输吃亏还可能因为带了编译器而更容易触发安全软件的扫描。把 JRE 裁剪到几十 MB 后配合 exe4j 的启动器整个应用目录看起来就是一个普通的绿色软件目录。老版本 JDK 的压缩参数是--compresszip-6比较新的 JDK 才用--compress2如果命令报参数不识别运行jlink --help看本机写法不要死记硬背。3. 对一个可复现的 Java 工程跑通 exe4j 最小打包步骤3.1 准备一个能独立运行的 JARexe4j 的输入通常是一个可执行 JAR。为了展示打包后的效果写一个会读取配置并打印java.home的小程序import java.io.FileInputStream; import java.io.InputStream; import java.util.Properties; public class HelloCli { public static void main(String[] args) throws Exception { Properties props new Properties(); try (InputStream in new FileInputStream(app.properties)) { props.load(in); } catch (Exception e) { System.err.println(警告: app.properties 缺失使用默认值); } System.out.println(Hello from exe4j, java.home System.getProperty(java.home)); System.out.println(配置项 env props.getProperty(env, dev)); } }这个类故意把java.home打印出来便于后面验证 exe4j 到底从哪个 JRE 启动。用下面的命令编译并打包javac --release 17 -encoding UTF-8 HelloCli.java jar cfe app.jar com.example.HelloCli .jar cfe中的e指定入口类f指定输出文件名最后一个圆点把当前目录里的 class 文件写进 jar。注意--release 17同时约束了 source 和 target这样即使你的 IDE 配了 JDK 21命令行产物仍然能被后面捆绑的 JDK 17 运行时识别。打包完成后用jar tf app.jar看一下 manifest 和 class 路径是否符合预期。3.2 exe4j 向导里的关键字段逐个说exe4j 的向导版本多界面文字有出入但核心字段是长期稳定的。先保持默认的应用模式不要选成 Windows Service除非你要做服务化运行。接着按顺序看几个影响行为的字段Executable name最终 exe 文件名建议不带空格例如demo.exe。Main Class必须是完全限定名com.example.HelloCli不能写成HelloCli.class。Classpath把app.jar加进来如果工程依赖第三方 jar把lib目录整体加进来。不要把私有 JRE 目录加进 Classpath它是运行时位置不是业务类位置。Search sequence把runtime\win64-jre放在第一个其余后备项按前面表格里的思路排列。VM Parameters常见值是-Xmx256m需要中文环境就补-Dfile.encodingUTF-8。如果你在 IDEA 里开发先确认 Project Structure 里的 Project SDK 和 Modules SDK 指向同一个 JDK 17再去跑 javac/jar 命令行否则会出现IDEA 里能编译、命令行编出来是另一版本的混乱。exe4j 的向导在配置完这些字段后会生成最终 exe并允许你回向导里反复修改后再构建。3.3 打包后的目录结构清单一个典型发布目录长这样路径作用app.exeexe4j 生成的启动器app.jar业务代码与依赖app.properties应用配置文件runtime\win64-jre\bin\server\jvm.dll私有 JVM 核心库runtime\win64-jre\release运行时版本信息将整个目录压缩发给客户客户解压之后直接双击app.exe。注意app.properties需要和 exe 在同一目录因为程序里用的是相对路径而java -jar时的当前工作目录可能不是 exe 目录。exe4j 提供 Working directory 配置为了减少问题通常把它设置成启动器所在目录。3.4 在没有 JDK的机器上怎么验证先在客户机上做一次环境探测确认 PATH 里没有 Javawhere java java -version set APP_HOME%~dp0 %APP_HOME%\app.exe如果where java没有输出说明这台机器确实没配置 JAVA_HOME也没往 PATH 里放 java.exe。此时app.exe能打印出java.home...runtime\win64-jre就说明私有 JRE 生效了。比较严谨的验收方式是拿一台干净的 Windows 沙箱复制目录、双击、看日志而不是在开发机上顺手点一下。4. 常见坑与关键参数找不到 JVM、版本不一致、控制台闪退4.1 Failed to locate the JVM是搜索序列或者 JVM 目录损坏exe4j 最典型的开局错误就是提示找不到 JVM。它表示启动器把配置里的搜索位置全部扫了一遍没有找到jvm.dll。常见原因有三个搜索序列里写的路径不对、私有 JRE 目录没有完整发布、32 位和 64 位不匹配。其中位数问题最容易骗人64 位 JDK 的 jvm.dll 不会被 32 位启动器加载exe4j 挑选 JVM 时会把位数作为筛选条件。排错先用这条命令检查 JVM 是否真实存在echo off setlocal set JRE%~dp0runtime\win64-jre if exist %JRE%\bin\server\jvm.dll ( echo jvm.dll exists %JRE%\bin\java.exe -version ) else ( echo jvm.dll missing: %JRE%\bin\server\jvm.dll )%~dp0是 bat 所在目录也就是 exe 所在目录加在私有 JRE 路径前面可以防止脚本在别的盘符调用时找不到路径。如果这个脚本打印jvm.dll exists但 exe4j 仍然报错把 exe4j 搜索序列里的所有项都去掉只留一个runtime\win64-jre再试。注意路径不要写成.\runtime\win64-jre引导程序对前导点号的处理不统一直接用最朴素的相对路径最稳。4.2 NoClassDefFoundError 说明 classpath 没跟上exe4j 已经找到了 JVM却在 main 执行前报NoClassDefFoundError这时候问题出在 Classpath 配置或 jar 自身的 manifest 上。用下面的命令检查 jar 内部jar tf app.jar | findstr /i META-INF/MANIFEST.MF com/example/HelloCli.class jar xf app.jar META-INF/MANIFEST.MF type META-INF\MANIFEST.MFjar tf输出 jar 里的文件清单确认 class 的包路径和 manifest 位置jar xf解压出 manifest用type看主类声明。常见问题是 jar 里根本没有 Main-Class 属性或者 exe4j 的 Main Class 填成了HelloCli而不是com.example.HelloCli。另一个容易被忽略的场景是依赖的第三方 jar 放在lib目录但 exe4j 的 Classpath 只加了app.jar这样 JVM 加载不到依赖类。把lib目录整体加入 Classpathexe4j 会按目录扫描里面的 jar。4.3 源发行版 17 需要目标发行版 17背后是字节码版本和运行时版本没对齐开发环境里常见的java: 警告: 源发行版 17 需要目标发行版 17以及团队里讨论jdk 降级到 17本质都是编译产物的 class 文件版本号高于运行时 JVM 支持的版本。JDK 17 的 class 文件主版本是 61JDK 11 是 55JDK 8 是 52exe4j 捆绑的私有 JRE 是 17却用 JDK 21 编译出主版本 65 的 class运行时就会拒绝加载。避免这个坑的唯一办法是让编译时的--release和运行时 JVM 保持一致。Maven 里对应maven.compiler.releaseGradle 里对应 JavaCompile 的options.release。如果你的项目必须降到 17先确认所有模块的 SDK 都指向 17再用下面命令重新编译打包javac --release 17 -encoding UTF-8 HelloCli.java jar cfe app.jar com.example.HelloCli .注意这里只改-source或-target不够必须用--release因为它会同时限制 JDK API 的引用范围避免编译期调用运行时没有的 API。如果已经出现UnsupportedClassVersionError检查 class 版本最直接的方法是javap -v看 class 文件的major version字段。4.4 双击闪退先让进程停下来看错误exe4j 支持 Console 和 GUI 两种执行类型。如果为了界面干净选了 GUI applicationSystem.out不会出现在任何窗口异常信息直接消失表现为双击之后窗口一闪而过。处理的第一步是先确认 exe4j 项目用 Console 类型重新生成一次再用cmd /k让窗口保持打开cmd /k app.exe/k的含义是执行完命令后不关闭窗口这样最后一屏的堆栈信息会留在屏幕上。如果产品必须用 GUI 模式更可靠的方式是在 main 入口统一捕获 Throwable 并写日志文件public static void main(String[] args) { try { new App().run(args); } catch (Throwable t) { t.printStackTrace(); System.out.println(异常已写入 error.log); try { Thread.sleep(5000); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } }这里加 5 秒阻塞是为了让双击场景下窗口停止闪退用户能看清错误提示。真正的 GUI 工程应当把异常同时打到日志文件不要依赖控制台。另外读取app.properties的代码如果以 jar 所在目录为基准要显式用System.getProperty(user.dir)或从代码来源推导出目录否则从快捷方式启动时会按其他目录找文件。5. 用 jlink jdeps 把私有 JRE 裁剪到最小再交给 exe4j5.1 自动计算模块依赖前面手动写--add-modules的问题在于依赖容易漏。对已有 jar 做依赖分析一条命令就够了%JAVA_HOME%\bin\jdeps --print-module-deps app.jar deps.txt set /p DEPSdeps.txt %JAVA_HOME%\bin\jlink --module-path %JAVA_HOME%\jmods --add-modules %DEPS% --strip-debug --no-header-files --compress2 --output runtime\win64-jreset /p把 deps.txt 第一行内容读进变量再传给 jlink。这个脚本适合模块依赖比较干净的工程。如果框架大量用了反射jdeps 的输出会缺运行时才发现的模块java.desktop或java.sql这类经常要手工补。我一般在 jdeps 输出后面追加,java.sql,java.naming作为保险成本只是压缩后体积稍微大一点换来的是少一次现场翻车。5.2 发布前需要确认的两件小事第一runtime\win64-jre目录里的release文件不要删exe4j 搜索 JVM 时靠它判断运行时版本和平台删掉会导致搜索失败。第二bin\server才是 JVM 启动路径exe4j 默认查找server目录下的jvm.dll老的client目录不要抱期望。还想再压体积可以加--strip-native-commands但代价是bin下不再有 java.exe以后在客户机上排查环境少了一个顺手工具所以通常保留。5.3 如何确认打包后的 exe 真的用了私有 JRE给启动器传一个 JVM 参数-XshowSettings:properties在cmd /k app.exe下运行窗口会滚出java.home ...\runtime\win64-jre一类信息。也可以把 HelloCli 里System.getProperty(java.home)输出重定向到文件对比它的值是否落在应用目录内。发布前的验收脚本可以这样写where java nul 21 set PATHC:\Windows\System32 app.exe run.log 21 findstr /i runtime\\win64-jre run.log把 PATH 清理到只剩系统目录模拟客户机上没有 Java 的情况如果 run.log 里还是出现私有 JRE 路径就说明 exe4j 的搜索序列优先取了捆绑运行时。这个脚本本身也是未来排查客户报障时的第一手证据值得和发布包一起留在现场。本文还有配套的精品资源点击获取