jpackage-maven-plugin
通用原生打包 Maven 插件:基于 OpenJDK 16+ 官方
jpackage工具,把任意标准 Java 应用打包成 内嵌最小运行时的跨平台自包含应用镜像(App Image / 绿色免安装版)。 目标机器无需安装 Java、无需配置环境变量,解压即用。
- 不绑定任何框架:不依赖 Spring Boot / JavaFX / Quarkus,只遵循纯 Java 与 Maven 标准;
- 智能开箱即用:自动从主 JAR 的
META-INF/MANIFEST.MF探测Main-Class,绝大多数项目 只需引入插件即可打包; - 跨平台自适应:自动识别 Windows / Linux / macOS,按平台组装参数并生成对应的启动脚本。
目录
环境要求
| 项目 | 要求 |
|---|---|
| 运行 Maven 的 JDK | JDK 16 及以上(jpackage 自 JDK 14 引入,16 起稳定;建议 17/21 LTS) |
| Maven | 3.6.3 及以上 |
| 打包目标平台 | 必须与构建主机平台一致(除非使用 Wine 交叉打包) |
jpackage 只能产出当前所在平台的可执行程序,这是 JDK 官方工具的限制:
- 在 Linux 上打包 → 得到 Linux 应用镜像;
- 在 Windows 上打包 → 得到 Windows 应用镜像;
- 在 macOS 上打包 → 得到 macOS 应用镜像。
需要多平台产物时,请在对应平台的 CI Runner 上分别构建。
快速开始(零配置)
只要项目能打出带 Main-Class 的 JAR,引入插件即可:
<build>
<plugins>
<plugin>
<groupId>com.mirenqinggege.maven</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>native-package</id>
<goals>
<goal>package-app</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
执行:
mvn clean package
插件会自动完成:定位 jpackage → 建立纯净输入目录 → 探测 Main-Class → 清洗版本号 →
组装命令行 → 调用 jpackage → 生成启动脚本 → 清理临时目录。
如果没有
Main-Class且未配置<mainClass>,插件会打印告警并交由jpackage自行从清单读取;此时若构建失败,请显式配置<mainClass>。
输出结构
默认产物位于 target/dist/<appName>/:
target/dist/hello-cli/
├── bin/
│ └── hello-cli # jpackage 生成的平台可执行文件
├── lib/
│ ├── runtime/ # 内嵌的最小 JRE(jlink 裁剪)
│ ├── app/
│ │ ├── hello-cli.jar # 主 JAR 与全部运行时依赖
│ │ └── hello-cli.cfg # jpackage 启动配置
│ └── ...
├── run.sh # 插件生成的启动脚本(Linux / macOS,755 权限)
└── hello-cli.desktop # 插件生成的桌面入口(仅 Linux)
Windows 平台对应生成 run.bat。
整个 <appName> 目录即为绿色免安装版:压缩后拷贝到目标机器解压,直接执行 run.sh
(或 run.bat)即可运行。
POM 配置示例
示例一:命令行工具(Linux 产物 + 自定义 JVM 参数)
<plugin>
<groupId>com.mirenqinggege.maven</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>native-package</id>
<goals>
<goal>package-app</goal>
</goals>
</execution>
</executions>
<configuration>
<appName>MyTool</appName>
<mainClass>com.example.MyTool</mainClass>
<vendor>Example Inc.</vendor>
<description>我的命令行工具</description>
<javaOptions>
<javaOption>-Xmx512m</javaOption>
<javaOption>-Duser.timezone=Asia/Shanghai</javaOption>
</javaOptions>
</configuration>
</plugin>
示例二:Windows 桌面应用(带图标、保留控制台)
<plugin>
<groupId>com.mirenqinggege.maven</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>native-package</id>
<goals>
<goal>package-app</goal>
</goals>
</execution>
</executions>
<configuration>
<icon>${project.basedir}/src/main/resources/app.ico</icon>
<winConsole>true</winConsole>
<!-- 不声明 mainClass,由插件从 MANIFEST.MF 自动探测 -->
</configuration>
</plugin>
示例三:已打成 fat jar 的项目
<configuration>
<!-- 依赖已在 fat jar 内,无需重复拷贝 -->
<includeDependencies>false</includeDependencies>
<!-- 自定义 finalName 时同步指定主 JAR 名 -->
<mainJar>${project.build.finalName}.jar</mainJar>
</configuration>
示例四:作为独立 goal 手动执行
不绑定生命周期,按需触发:
# 方式一:完整坐标(任何情况下都可用)
mvn com.mirenqinggege.maven:jpackage-maven-plugin:1.0.0:package-app
# 方式二:使用 goal 前缀(需先在 ~/.m2/settings.xml 中登记插件组)
mvn jpackage:package-app
启用前缀需在 settings.xml 中加入:
<pluginGroups>
<pluginGroup>com.mirenqinggege.maven</pluginGroup>
</pluginGroups>
示例五:CI 中按需跳过
mvn clean package -Djpackage.skip=true
完整可运行示例见 examples/hello-cli。
完整参数说明
核心参数
| 参数 | 属性名 | 默认值 | 说明 |
|---|---|---|---|
appName |
jpackage.appName |
${project.artifactId} |
应用名称,决定目录名与可执行文件名 |
mainJar |
jpackage.mainJar |
${project.build.finalName}.jar |
主 JAR 包文件名 |
mainClass |
jpackage.mainClass |
自动探测 | 主启动类;留空时从主 JAR 的 MANIFEST.MF 读取 Main-Class |
javaOptions |
— | 空 | List<String>,追加的自定义 JVM 参数 |
icon |
jpackage.icon |
空 | 图标文件:.ico(Windows)/ .png(Linux)/ .icns(macOS) |
type |
jpackage.type |
app-image |
jpackage 打包类型;app-image 即绿色免安装版 |
destinationDirectory |
jpackage.dest |
${project.build.directory}/dist |
产物输出目录 |
skip |
jpackage.skip |
false |
跳过原生打包 |
verbose |
jpackage.verbose |
false |
输出 jpackage 完整日志 |
编码与平台参数
| 参数 | 属性名 | 默认值 | 说明 |
|---|---|---|---|
injectUtf8Options |
jpackage.injectUtf8Options |
true |
注入全套 UTF-8 运行参数(见下) |
winConsole |
jpackage.winConsole |
true |
仅 Windows 目标平台附加 --win-console;Linux/macOS 永不传递 |
默认注入的 UTF-8 参数:
-Dfile.encoding=UTF-8
-Dstdout.encoding=UTF-8
-Dstderr.encoding=UTF-8
-Dsun.stdout.encoding=UTF-8
-Dsun.stderr.encoding=UTF-8
用户通过 javaOptions 追加的同名参数排在默认值之后,因此可以覆盖默认值。
输入目录与依赖
| 参数 | 默认值 | 说明 |
|---|---|---|
inputDirectory |
${project.build.directory}/jpackage-input |
纯净输入目录,插件会先清空再填充 |
includeDependencies |
true |
是否把 compile/runtime/system 作用域的 JAR 依赖一并拷贝 |
cleanInputDirectory |
true |
打包结束后删除临时输入目录 |
additionalArguments |
空 | List<String>,原样追加到 jpackage 命令行的参数 |
输入目录隔离:插件不会把
target/根目录直接作为--input,而是建立独立的jpackage-input/目录,只放入主 JAR 与依赖,避免classes/、maven-status/、 测试报告等编译产物被误打包进应用镜像。
启动脚本
| 参数 | 默认值 | 说明 |
|---|---|---|
generateLauncherScript |
true |
生成 run.sh / run.bat |
generateDesktopEntry |
true |
Linux 下额外生成 <appName>.desktop |
jpackage 定位与交叉打包
| 参数 | 属性名 | 默认值 | 说明 |
|---|---|---|---|
jpackageExecutable |
jpackage.jpackageExecutable |
自动探测 | 显式指定 jpackage 路径(可以是可执行文件、其所在目录或 JDK 根目录) |
useWine |
jpackage.useWine |
false |
在 Linux 上借助 Wine 调用 Windows 版 jpackage |
windowsJdkHome |
jpackage.windowsJdkHome |
空 | useWine=true 时必填,Windows 版 JDK 解压目录 |
jpackage 的自动探测优先级:
- 用户配置的
jpackageExecutable(配置错误时直接报错,不会静默回退); ${java.home}/bin/jpackage(当前运行 Maven 的 JDK,最可靠,兼容<jdk>/jre布局);$JAVA_HOME/bin/jpackage;- 系统
PATH上的jpackage。
Windows 下自动查找 jpackage.exe。
平台行为差异
| 行为 | Windows | Linux | macOS |
|---|---|---|---|
jpackage 文件名 |
jpackage.exe |
jpackage |
jpackage |
--win-console |
受 winConsole 控制 |
永不传递 | 永不传递 |
| 启动脚本 | run.bat(CRLF 换行) |
run.sh(LF 换行) |
run.sh(LF 换行) |
| 桌面入口 | — | <appName>.desktop |
— |
| 可执行权限 | 不适用 | 自动设为 755 | 自动设为 755 |
生成的 run.sh:
#!/bin/sh
# 由 jpackage-maven-plugin 自动生成:切换到脚本所在目录后启动应用
set -e
APP_HOME=$(cd "$(dirname "$0")" && pwd)
exec "$APP_HOME/bin/<appName>" "$@"
生成的 run.bat:
@echo off
rem 由 jpackage-maven-plugin 自动生成:切换控制台代码页为 UTF-8 后启动应用
chcp 65001 > nul
setlocal
"%~dp0<appName>.exe" %*
endlocal
chcp 65001 把 Windows 控制台代码页切换为 UTF-8,配合注入的 -Dfile.encoding=UTF-8 等
参数,彻底解决中文日志乱码问题。
在 Linux 上交叉打包 Windows 应用(Wine)
jpackage 无法跨平台产出,但可以在 Linux 上通过 Wine 运行 Windows 版 OpenJDK 的
jpackage.exe 来生成 Windows 应用镜像。
前提:
- Linux 构建机已安装 Wine(如
apt install wine64); - 已下载并解压 Windows 版 OpenJDK(JDK 而非 JRE,建议版本与目标一致)。
配置:
<configuration>
<useWine>true</useWine>
<windowsJdkHome>/opt/jdk-21.0.9-windows-x64</windowsJdkHome>
</configuration>
插件会:
- 校验
wine与<windowsJdkHome>/bin/jpackage.exe是否存在,缺失时快速失败并给出提示; - 通过
winepath -w把宿主路径转换为 Windows 路径(失败时回退到 Wine 默认的Z:盘映射); - 以
wine <jpackage.exe> ...形式执行,并设置WINEDEBUG=-all抑制 Wine 噪声; - 按 Windows 目标平台组装参数(附加
--win-console)并生成run.bat。
Wine 交叉打包属于尽力而为的方案,产物的可用性取决于 Wine 版本与 JDK 版本组合。 生产环境仍建议在真实 Windows Runner 上构建。
常见问题
Q:报错 Version [1.0.0-SNAPSHOT] is not valid?
A:插件已自动清洗版本号(1.0.0-SNAPSHOT → 1.0.0)。若你通过 additionalArguments
手工传递了版本参数,请自行确保其为纯数字点分格式。
Q:报错 Invalid Option: [--win-console]?
A:--win-console 只能在 Windows 上使用。插件已在 Linux/macOS 目标平台自动屏蔽该参数;
如果你通过 additionalArguments 手工加入了它,请移除。
Q:目标机器运行时报 NoClassDefFoundError?
A:说明有依赖没被打进镜像。检查依赖作用域是否为 provided 或 test(这两者不会被拷贝),
或项目是否使用了 shade/assembly 打成 fat jar(此时应设置
<includeDependencies>false</includeDependencies>)。
Q:想在 Windows 上打包但 mvn 运行的 JDK 不是 16+?
A:插件的 jpackageExecutable 允许指向另一个 JDK:
<jpackageExecutable>C:\jdk-21\bin\jpackage.exe</jpackageExecutable>。
Q:构建速度慢?
A:jpackage 需要对内嵌运行时做 jlink 裁剪并拷贝数百个文件,属正常现象。日常开发可用
-Djpackage.skip=true 跳过。
Q:中文输出乱码?
A:确认不要通过 javaOptions 覆盖掉 -Dfile.encoding=UTF-8;Windows 上请使用插件生成的
run.bat(其中含 chcp 65001)而非直接双击 bin\<appName>.exe。
发布到 Maven 中央仓库
本项目已按 Maven Central 的要求配置完毕:name / description / url / licenses /
developers / scm 齐全,sources 与 javadoc 构件随包附加,GPG 签名在 release
profile 中启用。
1. 前置准备
-
注册命名空间:登录 https://central.sonatype.com,添加并验证命名空间
com.mirenqinggege(可通过 DNS TXT 记录或代码托管平台账号验证;本项目的 SCM 地址为git.mirenqinggege.com,建议用域名方式验证)。 -
生成 Portal Token:在 Central Portal 的 Account → Generate User Token 中生成, 然后写入
~/.m2/settings.xml:<servers> <server> <id>central</id> <username>Portal Token 的用户名</username> <password>Portal Token 的密码</password> </server> </servers> -
准备 GPG 密钥:确认本地已有私钥,且公钥已发布到密钥服务器:
gpg --list-secret-keys --keyid-format=long gpg --keyserver keyserver.ubuntu.com --send-keys <你的KEYID>
2. 发布
mvn clean deploy -P release
该命令会:编译 → 单元测试 → 生成 plugin.xml → 打包 jar / sources / javadoc →
GPG 签名(release profile)→ 上传到 Central Portal → 等待校验通过。
默认 autoPublish=false,构件校验通过后会停留在 Portal 的待发布状态,
需在 https://central.sonatype.com/publishing 手动点击 Publish;
若希望校验通过即自动发布,把 pom.xml 中的 <autoPublish> 改为 true。
3. 未使用 release profile 时的行为
mvn clean verify # 只构建与测试,不签名(默认 profile 会把 sign 执行置为 phase=none)
mvn clean install # 安装到本地仓库,同样不签名
4. 覆盖签名密钥
mvn clean deploy -P release -Dgpg.keyname=ABCDEF0123456789
5. 版本升级
发布新版本时需同步修改三处,避免 SCM 标签与实际发布不一致:
pom.xml的<version>;pom.xml的<scm><tag>;README.md中示例使用的jpackage.plugin.version(examples/hello-cli/pom.xml)。
插件工程自身构建
mvn clean verify # 编译 + 单元测试 + 生成插件描述符
mvn install # 安装到本地仓库,供其他项目引用
mvn com.mirenqinggege.maven:jpackage-maven-plugin:1.0.0:help # 查看 goal 帮助
源码结构
src/main/java/com/mirenqinggege/maven/jpackage/
├── PackageAppMojo.java # 核心 Mojo:流程编排与错误处理
└── internal/
├── Platform.java # 平台探测与平台差异
├── JdkToolLocator.java # jpackage 可执行文件定位
├── VersionSanitizer.java # Maven 版本号 → jpackage 版本号
├── JarManifestInspector.java # MANIFEST.MF 解析(Main-Class / Class-Path)
├── JpackageOptions.java # 命令行组装(平台自适应、UTF-8 注入)
├── LauncherScriptGenerator.java # run.sh / run.bat / .desktop 生成
├── FileTreeUtils.java # 目录清理与拷贝
├── FilePermissions.java # 跨平台 POSIX 权限设置
├── JpackageExecutor.java # 进程调用抽象(可注入,便于测试)
└── WineSupport.java # Wine 交叉打包支持
设计上把「可测试的纯逻辑」全部下沉到 internal 包,Mojo 只负责编排:这使得命令行组装、
清单解析、脚本生成、版本清洗等核心行为都能在无 Maven 容器、无真实 jpackage 的条件下被
单元测试覆盖(共 103 个测试用例)。
编写测试的关键注入点
PackageAppMojo 的配置字段为包级可见,测试可直接赋值;另外提供三个注入点:
executor:替换进程执行器,避免真的启动jpackage;environment:替换java.home/JAVA_HOME;platformOverride:覆盖平台探测结果,用于在任意机器上验证 Windows 分支逻辑。
示例见 src/test/java/com/mirenqinggege/maven/jpackage/PackageAppMojoTest.java。