# 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,按平台组装参数并生成对应的启动脚本。 --- ## 目录 1. [环境要求](#环境要求) 2. [快速开始(零配置)](#快速开始零配置) 3. [输出结构](#输出结构) 4. [POM 配置示例](#pom-配置示例) 5. [完整参数说明](#完整参数说明) 6. [平台行为差异](#平台行为差异) 7. [在 Linux 上交叉打包 Windows 应用(Wine)](#在-linux-上交叉打包-windows-应用wine) 8. [常见问题](#常见问题) 9. [插件工程自身构建](#插件工程自身构建) --- ## 环境要求 | 项目 | 要求 | | --- | --- | | 运行 Maven 的 JDK | **JDK 16 及以上**(`jpackage` 自 JDK 14 引入,16 起稳定;建议 17/21 LTS) | | Maven | 3.6.3 及以上 | | 打包目标平台 | 必须与构建主机平台一致(除非使用 [Wine 交叉打包](#在-linux-上交叉打包-windows-应用wine)) | `jpackage` 只能产出**当前所在平台**的可执行程序,这是 JDK 官方工具的限制: - 在 Linux 上打包 → 得到 Linux 应用镜像; - 在 Windows 上打包 → 得到 Windows 应用镜像; - 在 macOS 上打包 → 得到 macOS 应用镜像。 需要多平台产物时,请在对应平台的 CI Runner 上分别构建。 ## 快速开始(零配置) 只要项目能打出带 `Main-Class` 的 JAR,引入插件即可: ```xml com.mirenqinggege.maven jpackage-maven-plugin 1.0.0 native-package package-app ``` 执行: ```bash mvn clean package ``` 插件会自动完成:定位 `jpackage` → 建立纯净输入目录 → 探测 `Main-Class` → 清洗版本号 → 组装命令行 → 调用 `jpackage` → 生成启动脚本 → 清理临时目录。 > 如果没有 `Main-Class` 且未配置 ``,插件会打印告警并交由 `jpackage` > 自行从清单读取;此时若构建失败,请显式配置 ``。 ## 输出结构 默认产物位于 `target/dist//`: ``` 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`。 整个 `` 目录即为绿色免安装版:压缩后拷贝到目标机器解压,直接执行 `run.sh` (或 `run.bat`)即可运行。 ## POM 配置示例 ### 示例一:命令行工具(Linux 产物 + 自定义 JVM 参数) ```xml com.mirenqinggege.maven jpackage-maven-plugin 1.0.0 native-package package-app MyTool com.example.MyTool Example Inc. 我的命令行工具 -Xmx512m -Duser.timezone=Asia/Shanghai ``` ### 示例二:Windows 桌面应用(带图标、保留控制台) ```xml com.mirenqinggege.maven jpackage-maven-plugin 1.0.0 native-package package-app ${project.basedir}/src/main/resources/app.ico true ``` ### 示例三:已打成 fat jar 的项目 ```xml false ${project.build.finalName}.jar ``` ### 示例四:作为独立 goal 手动执行 不绑定生命周期,按需触发: ```bash # 方式一:完整坐标(任何情况下都可用) mvn com.mirenqinggege.maven:jpackage-maven-plugin:1.0.0:package-app # 方式二:使用 goal 前缀(需先在 ~/.m2/settings.xml 中登记插件组) mvn jpackage:package-app ``` 启用前缀需在 `settings.xml` 中加入: ```xml com.mirenqinggege.maven ``` ### 示例五:CI 中按需跳过 ```bash mvn clean package -Djpackage.skip=true ``` 完整可运行示例见 [`examples/hello-cli`](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`,追加的自定义 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`,原样追加到 jpackage 命令行的参数 | > **输入目录隔离**:插件不会把 `target/` 根目录直接作为 `--input`,而是建立独立的 > `jpackage-input/` 目录,只放入主 JAR 与依赖,避免 `classes/`、`maven-status/`、 > 测试报告等编译产物被误打包进应用镜像。 ### 启动脚本 | 参数 | 默认值 | 说明 | | --- | --- | --- | | `generateLauncherScript` | `true` | 生成 `run.sh` / `run.bat` | | `generateDesktopEntry` | `true` | Linux 下额外生成 `.desktop` | ### jpackage 定位与交叉打包 | 参数 | 属性名 | 默认值 | 说明 | | --- | --- | --- | --- | | `jpackageExecutable` | `jpackage.jpackageExecutable` | 自动探测 | 显式指定 jpackage 路径(可以是可执行文件、其所在目录或 JDK 根目录) | | `useWine` | `jpackage.useWine` | `false` | 在 Linux 上借助 Wine 调用 Windows 版 jpackage | | `windowsJdkHome` | `jpackage.windowsJdkHome` | 空 | `useWine=true` 时必填,Windows 版 JDK 解压目录 | jpackage 的自动探测优先级: 1. 用户配置的 `jpackageExecutable`(配置错误时**直接报错**,不会静默回退); 2. `${java.home}/bin/jpackage`(当前运行 Maven 的 JDK,最可靠,兼容 `/jre` 布局); 3. `$JAVA_HOME/bin/jpackage`; 4. 系统 `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 换行) | | 桌面入口 | — | `.desktop` | — | | 可执行权限 | 不适用 | 自动设为 755 | 自动设为 755 | 生成的 `run.sh`: ```sh #!/bin/sh # 由 jpackage-maven-plugin 自动生成:切换到脚本所在目录后启动应用 set -e APP_HOME=$(cd "$(dirname "$0")" && pwd) exec "$APP_HOME/bin/" "$@" ``` 生成的 `run.bat`: ```bat @echo off rem 由 jpackage-maven-plugin 自动生成:切换控制台代码页为 UTF-8 后启动应用 chcp 65001 > nul setlocal "%~dp0.exe" %* endlocal ``` `chcp 65001` 把 Windows 控制台代码页切换为 UTF-8,配合注入的 `-Dfile.encoding=UTF-8` 等 参数,彻底解决中文日志乱码问题。 ## 在 Linux 上交叉打包 Windows 应用(Wine) `jpackage` 无法跨平台产出,但可以在 Linux 上通过 Wine 运行 **Windows 版 OpenJDK** 的 `jpackage.exe` 来生成 Windows 应用镜像。 前提: 1. Linux 构建机已安装 Wine(如 `apt install wine64`); 2. 已下载并解压 Windows 版 OpenJDK(JDK 而非 JRE,建议版本与目标一致)。 配置: ```xml true /opt/jdk-21.0.9-windows-x64 ``` 插件会: 1. 校验 `wine` 与 `/bin/jpackage.exe` 是否存在,缺失时快速失败并给出提示; 2. 通过 `winepath -w` 把宿主路径转换为 Windows 路径(失败时回退到 Wine 默认的 `Z:` 盘映射); 3. 以 `wine ...` 形式执行,并设置 `WINEDEBUG=-all` 抑制 Wine 噪声; 4. 按 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(此时应设置 `false`)。 **Q:想在 Windows 上打包但 `mvn` 运行的 JDK 不是 16+?** A:插件的 `jpackageExecutable` 允许指向另一个 JDK: `C:\jdk-21\bin\jpackage.exe`。 **Q:构建速度慢?** A:`jpackage` 需要对内嵌运行时做 jlink 裁剪并拷贝数百个文件,属正常现象。日常开发可用 `-Djpackage.skip=true` 跳过。 **Q:中文输出乱码?** A:确认不要通过 `javaOptions` 覆盖掉 `-Dfile.encoding=UTF-8`;Windows 上请使用插件生成的 `run.bat`(其中含 `chcp 65001`)而非直接双击 `bin\.exe`。 ## 发布到 Maven 中央仓库 本项目已按 Maven Central 的要求配置完毕:`name` / `description` / `url` / `licenses` / `developers` / `scm` 齐全,`sources` 与 `javadoc` 构件随包附加,GPG 签名在 `release` profile 中启用。 ### 1. 前置准备 1. **注册命名空间**:登录 ,添加并验证命名空间 `com.mirenqinggege`(可通过 DNS TXT 记录或代码托管平台账号验证;本项目的 SCM 地址为 `git.mirenqinggege.com`,建议用域名方式验证)。 2. **生成 Portal Token**:在 Central Portal 的 *Account → Generate User Token* 中生成, 然后写入 `~/.m2/settings.xml`: ```xml central Portal Token 的用户名 Portal Token 的密码 ``` 3. **准备 GPG 密钥**:确认本地已有私钥,且公钥已发布到密钥服务器: ```bash gpg --list-secret-keys --keyid-format=long gpg --keyserver keyserver.ubuntu.com --send-keys <你的KEYID> ``` ### 2. 发布 ```bash mvn clean deploy -P release ``` 该命令会:编译 → 单元测试 → 生成 `plugin.xml` → 打包 `jar` / `sources` / `javadoc` → GPG 签名(`release` profile)→ 上传到 Central Portal → 等待校验通过。 默认 `autoPublish=false`,构件校验通过后会停留在 Portal 的待发布状态, 需在 手动点击 **Publish**; 若希望校验通过即自动发布,把 `pom.xml` 中的 `` 改为 `true`。 ### 3. 未使用 release profile 时的行为 ```bash mvn clean verify # 只构建与测试,不签名(默认 profile 会把 sign 执行置为 phase=none) mvn clean install # 安装到本地仓库,同样不签名 ``` ### 4. 覆盖签名密钥 ```bash mvn clean deploy -P release -Dgpg.keyname=ABCDEF0123456789 ``` ### 5. 版本升级 发布新版本时需同步修改三处,避免 SCM 标签与实际发布不一致: - `pom.xml` 的 ``; - `pom.xml` 的 ``; - `README.md` 中示例使用的 `jpackage.plugin.version`(`examples/hello-cli/pom.xml`)。 ## 插件工程自身构建 ```bash 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`](src/test/java/com/mirenqinggege/maven/jpackage/PackageAppMojoTest.java)。