生成 Windows EXE 启动器
打包时可以为受保护的应用生成一个原生 Windows 启动程序。它是纯 Win32 可执行文件,双击即可运行,你的用户不需要先安装 Java,也不必接触 run.bat。
EXE 是可选项,默认不生成。启用后,生成的包里仍然保留 run.sh、run.command 和 run.bat。两种启动方式使用完全相同的归档结构、类路径、主类和 JVM 启动选项。
1. 适用范围
| 应用类型 | Java 应用、Spring Boot(三种布局均可)、Tomcat |
| 目标平台 | windows-x64、windows-x86、windows-aarch64 |
| 不支持 | 类库加密 —— 单个 .p4jx 归档不是可启动的应用目录 |
必须至少选择一个 Windows 目标平台。同时选择 Linux 和 macOS 目标没有问题:这些平台的包照常生成,只是不带 EXE,仍通过原有脚本启动。如果一个 Windows 目标都没选,任务会直接报错。
2. GUI 操作
- 在输入文件与目标平台页面勾选生成 Windows 应用 EXE(x64/x86/ARM64),并至少选择一个 Windows 目标平台。
- 这个选项位于简单模式与高级模式的分叉之前,因此两种模式都可以生成 EXE。
- 勾选后会多出一个 Windows EXE 启动器页面,用于填写文件名、启动器模式、图标和 Windows 版本信息。不勾选则直接进入最终确认页。
- 启用 EXE 后,JVM 启动选项输入框只出现在这个启动器页面上,这样同一项选项就不会有两个入口。填写的内容会同时写入 EXE 和启动脚本。
- 最终确认页会列出实际会生成 EXE 的平台,以及你填写过的启动器字段。
3. CLI 示例
最简形式,只需要 --windows-exe:
p4j --target-platform windows-x64 javaapp app.jar dist --windows-exe
指定文件名和窗口模式:
p4j --target-platform windows-x64 javaapp app.jar dist \
--windows-exe \
--exe-name MyApp.exe \
--exe-mode gui \
--exe-icon assets/app.ico
一次生成三种 Windows 架构,并填写完整的版本资源:
p4j --target-platform windows-x64,windows-x86,windows-aarch64 \
springboot app.jar dist \
--windows-exe \
--exe-name MyService \
--exe-file-version 1.4.2.0 \
--exe-product-version 1.4.2.0 \
--exe-company 'Example Inc.' \
--exe-product 'Example Service' \
--exe-description 'Example background service' \
--exe-copyright 'Copyright (C) 2026 Example Inc.'
Tomcat 包同样支持,生成的 EXE 会在前台启动内嵌的 Tomcat:
p4j --target-platform windows-x64 tomcat app.war dist --context /demo --windows-exe
4. 选项说明
| 选项 | 说明 |
|---|---|
--windows-exe | 生成 Windows 应用 EXE |
--exe-name <name> | EXE 文件名;留空时使用输入文件名,Tomcat 包使用 tomcat |
--exe-mode <mode> | console(默认)或 gui |
--exe-icon <ico> | 可选的 Windows 图标,.ico 格式 |
--exe-file-version <a.b.c.d> | PE 文件版本:1 到 4 段数字,每段取值 0 到 65535。留空表示 0.0.0.0。 |
--exe-product-version <a.b.c.d> | PE 产品版本,规则同上 |
--exe-company <text> | 公司名称 |
--exe-product <text> | 产品名称 |
--exe-description <text> | 文件说明,显示在任务管理器和文件属性对话框中 |
--exe-copyright <text> | 版权声明 |
任何一个 --exe-* 选项都会自动启用 EXE 生成,因此不必再额外写 --windows-exe。
文件名必须是纯文件名,不能包含 /、\ 或任何目录成分;如果省略了 .exe 后缀,工具会自动补上。
两个版本号字段遵循同一条规则:1 到 4 段用半角句点分隔的数字,每段取值 0 到 65535,例如 1.0.0.1。Windows 把每一段存为 16 位无符号整数,因此像 v1.0、1.0-beta 这类含字母或空格的写法会被拒绝。留空表示 0.0.0.0。这两个字段在打包开始前就会校验,因此填错会立刻暴露,而不是打到一半才失败。
5. 输出结构
以 Java 应用为例,EXE 与启动脚本一起放在包的根目录下:
dist/
├── MyApp.exe # 新增的 Windows 启动程序
├── app.p4jx
├── vlxjre/
├── lib/
├── run.sh
├── run.command
├── run.bat
└── README.md
包内的 README.md 会列出已生成的 EXE 文件名,以及代码签名方面的提示。
6. 运行行为
EXE 所在的目录就是应用的根目录。启动器只接受根目录之内的运行时、归档、类路径和工作目录;指向包外的路径,以及被符号链接或 junction 替换过的路径,都会被拒绝,进程不会启动。因此整个包可以随意移动或改名,但不能把 EXE 单独复制出去使用。
| 方面 | 行为 |
|---|---|
| 运行时 | 始终使用包内的 vlxjre\bin\java.exe,不使用系统 Java,也不读取 JAVA_HOME。 |
| 命令行参数 | 传给 EXE 的参数会追加在应用自身参数之后。 |
| 控制台模式 | 继承当前控制台的标准输入输出,等待应用结束,并返回它的退出码。 |
| GUI 模式 | 不创建控制台窗口,适合桌面应用。 |
| 环境变量 | 启动前清除 JAVA_TOOL_OPTIONS、_JAVA_OPTIONS、JDK_JAVA_OPTIONS 和 CLASSPATH,防止外部注入改变启动参数。 |
EXE 的 JVM 启动选项在打包时就已固定,并与参数块一起签名、在运行时校验,部署后无法修改。APP_JAVA_OPTS 只对 run.bat 有效,EXE 不会读取它。需要修改 EXE 的 JVM 启动选项,请重新打包。
出于同样的原因,-javaagent、-agentlib、-agentpath、-Xbootclasspath 和 --patch-module 不允许写入 EXE 的启动参数。试图用 --jvm-option 传入这些参数,打包会立即失败。
7. 代码签名
Protector4J 不会为生成的 EXE 添加发布者签名,也不会接触任何 Authenticode 凭据。图标、版本资源、启动参数等所有 PE 修改都在打包阶段完成,因此签名必须放在最后一步。
- 完成打包,并确认 EXE 能够正常启动应用。
- 使用你自己的证书做 Authenticode 签名,并附带 RFC3161 时间戳。
- 按 Windows 的
/pa策略验证签名。
签名完成后不要再修改这个 PE 文件,任何改动都会让签名失效。需要更换图标或版本号时,请重新打包并重新签名。
8. 任务文件字段
导出的 p4j-task.yml 使用第 2 版格式,启用 EXE 时会保存以下字段。第 1 版的任务文件仍然可以正常读取。
windowsExe: true
exeName: MyApp.exe
exeMode: console
exeIcon: assets/app.ico
exeFileVersion: 1.4.2.0
exeProductVersion: 1.4.2.0
exeCompany: Example Inc.
exeProduct: Example Service
exeDescription: Example background service
exeCopyright: Copyright (C) 2026 Example Inc.
9. 常见问题
| 现象 | 原因与处理 |
|---|---|
| 提示需要选择 Windows 目标平台 | 已启用 EXE,但没有选中 windows-x64、windows-x86 或 windows-aarch64。 |
| 提示版本号格式不正确 | 版本字段中含有字母或空格,或超过 4 段,或某一段不在 0 到 65535 之间。 |
| 提示 EXE 名称只能是文件名 | 文件名中含有路径分隔符,请改成不带目录的纯文件名。 |
| 提示找不到图标文件 | --exe-icon 指向的 .ico 不存在,请检查路径。 |
| 双击后没有窗口,应用立即退出 | 应用在 GUI 模式下启动失败。改用控制台模式重新打包,以查看错误输出。 |
| 单独复制出来的 EXE 无法运行 | 启动器要求运行时和归档位于同一个包目录中,请复制整个输出目录。 |