Files
jpackage-maven-plugin/README.md
T
zkhandzkh 44f6205f89 feat: 初始化 jpackage-maven-plugin 通用原生打包插件
基于 OpenJDK 16+ 官方 jpackage 工具,将任意标准 Java 应用打包为内嵌最小运行时的
跨平台自包含应用镜像(app-image 绿色免安装版),不绑定任何应用框架。

核心能力:
- 零配置:自动从主 JAR 的 META-INF/MANIFEST.MF 探测 Main-Class
- 环境定位:优先 ${java.home}/bin/jpackage,兼容 JRE 布局、JAVA_HOME 与 PATH
- 输入目录隔离:使用独立的 target/jpackage-input,避免编译产物被误打包
- 版本清洗:1.0.0-SNAPSHOT 等 Maven 版本号自动转为 jpackage 合法版本
- 编码防御:默认注入全套 UTF-8 运行参数,规避跨平台中文乱码
- 平台自适应:--win-console 仅在 Windows 目标平台传递
- 启动脚本:生成 run.sh(755)与 CRLF 换行的 run.bat(含 chcp 65001)
- Wine 交叉打包:支持在 Linux 上借助 Wine 产出 Windows 应用镜像

交付内容:
- 插件工程与面向 Maven Central 的完整 pom 配置
- 核心 Mojo 与 10 个 internal 工具类
- 103 个单元测试用例
- examples/hello-cli 可运行示例与中文 README

Generated with AI

Co-Authored-By: zkh <zkh@mirenqinggege.com>
2026-10-14 14:46:44 +08:00

17 KiB
Raw Blame History

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 配置示例
  5. 完整参数说明
  6. 平台行为差异
  7. 在 Linux 上交叉打包 Windows 应用(Wine)
  8. 常见问题
  9. 插件工程自身构建

环境要求

项目 要求
运行 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 的自动探测优先级:

  1. 用户配置的 jpackageExecutable(配置错误时直接报错,不会静默回退);
  2. ${java.home}/bin/jpackage(当前运行 Maven 的 JDK,最可靠,兼容 <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 换行)
桌面入口 — <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 应用镜像。

前提:

  1. Linux 构建机已安装 Wine(如 apt install wine64);
  2. 已下载并解压 Windows 版 OpenJDK(JDK 而非 JRE,建议版本与目标一致)。

配置:

<configuration>
    <useWine>true</useWine>
    <windowsJdkHome>/opt/jdk-21.0.9-windows-x64</windowsJdkHome>
</configuration>

插件会:

  1. 校验 wine 与 <windowsJdkHome>/bin/jpackage.exe 是否存在,缺失时快速失败并给出提示;
  2. 通过 winepath -w 把宿主路径转换为 Windows 路径(失败时回退到 Wine 默认的 Z: 盘映射);
  3. 以 wine <jpackage.exe> ... 形式执行,并设置 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(此时应设置 <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. 前置准备

  1. 注册命名空间:登录 https://central.sonatype.com,添加并验证命名空间 com.mirenqinggege(可通过 DNS TXT 记录或代码托管平台账号验证;本项目的 SCM 地址为 git.mirenqinggege.com,建议用域名方式验证)。

  2. 生成 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>
    
  3. 准备 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。