JavaFX应用程序打包

——使用jlink和jpackage

摘要

示例演示基于java21和JavaFX 21,使用jdk的jlink和jpackage命令打包 FireX AI 桌面程序发布包。

FireX AI 下载体验

运行环境

JDK: Java21 [具体版本:OpenJDK Temurin-21.0.1+12 (build 21.0.1+12-LTS)]

JavaFX: JavaFX 21 [具体版本:javafx-sdk-21.0.1 和 javafx-jmods-21.0.1]

打包命令:jlink 和 jpackage

基本概念

什么是 jlink?

官方定义:https://docs.oracle.com/en/java/javase/11/tools/jlink.html

You can use the jlink tool to assemble and optimize a set of modules and their dependencies into a custom runtime image.

从定义可知,使用jlink工具,可以将一组模块及其依赖项,组装和优化到自定义运行时映像中。

说人话就是:使用 jlink 命令,可以组装定制模块化Java 程序的运行时镜像。它包含了基础运行时(即JRE) 和你自己的应用程序。

那么为什么需要组装和定制运行时镜像呢?

因为,

一、我们不得不这样:从 Java9 推出模块化后,Java 官方只有一个JDK安装包的不再提供单独的JRE了。想要打包自带JRE的应用程序包,不能再用Java9 之前的方式了。(Java8:怎么感觉是在说我呢?)

二、这样能带来的好处:
好处 1:可以大大缩小最终的应用发布程序包大小,大约一般缩小 40%-50%左右,视不同程序有所不同(稍后细说);
好处 2:运行镜像可以定制只包括用到 jdk 模块,不用到的 jdk 模块,可以剔除,这在 java9 模块化之前,是做不到的。

什么是 jpackage?

生成适合主机系统的应用程序包,模块化和非模块化的应用程序均可用。

简单的说,通过 jpackage 可以将应用程序打包成 windows 平台的 exe 安装包,macos 的 dmg或 pkg 安装包。

打包步骤

1. 用 jlink 组装和定制运行时镜像:

命令示例

export PATH_TO_FX_MODS=/Users/bbstone/devenv/openjfx-21.0.1/javafx-jmods-21.0.1;\

export FIREX_LIB=/Users/bbstone/workdir/FIREX_LIB;\

jlink --module-path ${PATH_TO_FX_MODS}:${FIREX_LIB}/modules \

  --add-modules firex.gui \

  --output /Users/bbstone/workdir/FIREX_LIB/dist \

  --launcher fireX=firex.gui/cn.bbstone.firex.gui.FireXStarter \

  --strip-debug

环境变量

PATH_TO_FX_MODS:javafx jmods 的位置

javafx-jmods-21.0.1目录内容如下:

FIREX_LIB:FireX AI应用程序的 jar 位置

FIREX_LIB/modules目录内容如下:

参数说明

--module-path:模块路径。如果未指定,将使用 JDK 的 jmods 目录(如果存在该目录)。如果指定,但它不包含 java.base 模块,则将添加 JDK 的 jmods 目录(如果存在该目录)。
--add-modules:除了初始模块之外要解析的根模块。
--output:输出路径的位置(也是jpackage --runtime-image 参数需要指定的位置)。
--launcher <名称>=<模块>[/<主类>]:为模块和主类添加给定名称的启动程序命令(如果指定) 。
--strip-debug:去除调试信息。

注意:

  • 以上命令执行需要应用程序依赖的 jar 都是模块化的,如果没有模块化,需要做转换。
  • 将非模块化的 jar 转换成模块化的 jar 的方法,请参考附录I。
  • 有关 jlink命令的参数和更多用法,请在安装了 JDK 的电脑运行命令 “jlink -h”进行查看.或查看官方文档:https://docs.oracle.com/en/java/javase/11/tools/jlink.html

2.用 jpackage 将 jlink 组装的运行时镜像打包成 macos 的安装包dmg 和 pkg

命令示例

% cd /Users/bbstone/workdir/FIREX_LIB
% jpackage  --runtime-image dist  --type pkg --name FireX-AI_macos-aarch64 --app-version 1.0.0 --module firex.gui/cn.bbstone.firex.gui.FireXStarter \
  --java-options -Dprofiles.active=prod --icon /Users/bbstone/workdir/FIREX_LIB/app/logo.icns --vendor bbstone.cn

参数说明

--runtime-image:上面用 jlink 组装的运行时镜像的目录,例如:dist
--type:根据 jpackage 命令运行的操作系统,有不同的值,macos 有效值为:app-image, dmg, pkg。
--name:最终打包的名字(不包括版本号)
--app-version:版本号(macos 上开头不能是 0,例如,想要设置版本位0.3.0是不可以的必须>0的版本号开头)
--module firex.gui/cn.bbstone.firex.gui.FireXStarter:启动程序的 模块/程序入口
--java-options -Dprofiles.active=prod:要传递到 Java 运行时的选项,可以多次使用此选项。
--icon /Users/bbstone/workdir/FIREX_LIB/app/logo.icns:应用程序包图标的路径(绝对路径或相对于当前目录的路径)
--vendor bbstone.cn:应用程序的供应商

注意:

  • 有关 jpackage 命令的参数和更多用法,请在安装了 JDK 的电脑运行命令 “jpackage -h”进行查看

MacOS 安装未签名的 FireX AI 客户端过程

安装 DMG 格式的安装包

将 FireX AI.app 图标拖动到 Applications 文件夹进行安装,得到提示如下:

点击“好”按钮后,在“安全与隐私”中看到如下:

点击“仍要打开”,再提示如下:

继续点击“打开”按钮,即可打开 FireX AI 客户端。至此,dmg 格式的FireX AI 安装完成。

安装 PKG 格式的安装包

鼠标双击 “FireX-AI_macos-aarch64-0.3.0 (2).pkg”安装,得到提示:

提示身份不明的开发者,在“安全与隐私”设置里,看到如下所示:

点击“仍要打开”按钮,得到如下提示:

点击“打开”按钮,继续安装

接下来按照引导一步步安装完即可。

在应用程序种看到如下图标,点击,即可打开 FireX AI客户端。至此,pkg 格式的FireX AI 安装完成。

附录 I:非模块化的 jar 转换成模块化

以下通过 maven 插件moditect演示如何将hankcs lucene 分词器,apache commons lang3,hutool 转换成模块的 jar:

<plugin>
    <!-- https://github.com/moditect/moditect -->
    <groupId>org.moditect</groupId>
    <artifactId>moditect-maven-plugin</artifactId>
    <version>1.0.0.Final</version>
    <executions>
        <execution>
            <id>add-module-infos</id>
            <phase>generate-resources</phase>
            <goals>
                <goal>add-module-info</goal>
            </goals>
            <configuration>
                <overwriteExistingFiles>true</overwriteExistingFiles>
                <outputDirectory>${firex.lib}/modules</outputDirectory>
                <modules>
                    <module>
                        <artifact>
                            <groupId>com.hankcs</groupId>
                            <artifactId>hanlp</artifactId>
                            <version>${hanlp.version}</version>
                        </artifact>
                        <moduleInfoSource>
                            module com.hankcs.hanlp {
                            requires java.logging;
                            exports com.hankcs.hanlp;
                            exports com.hankcs.hanlp.seg;
                            exports com.hankcs.hanlp.seg.common;
                            exports com.hankcs.hanlp.tokenizer;
                            exports com.hankcs.hanlp.corpus.io;
                            exports com.hankcs.hanlp.utility;
                            exports com.hankcs.hanlp.corpus.tag;
                            exports com.hankcs.hanlp.collection.trie.bintrie;
                            }
                        </moduleInfoSource>
                    </module>
                    <module>
                        <artifact>
                            <groupId>com.hankcs.nlp</groupId>
                            <artifactId>hanlp-lucene-plugin</artifactId>
                            <version>${hanlp-lucene-plugin.version}</version>
                        </artifact>
                        <moduleInfoSource>
                            module hanlp.lucene.plugin {
                            requires org.apache.lucene.core;
                            requires com.hankcs.hanlp;
                            exports com.hankcs.lucene;
                            }
                        </moduleInfoSource>
                    </module>
                    <module>
                        <artifact>
                            <groupId>org.apache.commons</groupId>
                            <artifactId>commons-lang3</artifactId>
                            <version>${commons-lang3.version}</version>
                        </artifact>
                        <moduleInfoSource>
                            module org.apache.commons.lang3 {
                            exports org.apache.commons.lang3;
                            }
                        </moduleInfoSource>
                    </module>
                    <module>
                        <artifact>
                            <groupId>cn.hutool</groupId>
                            <artifactId>hutool-core</artifactId>
                            <version>${hutool.version}</version>
                        </artifact>
                        <moduleInfoSource>
                            module cn.hutool.core {
                            exports cn.hutool.core.util;
                            exports cn.hutool.core.date;
                            exports cn.hutool.core.collection;
                            }
                        </moduleInfoSource>
                    </module>
                    <module>
                        <artifact>
                            <groupId>cn.hutool</groupId>
                            <artifactId>hutool-crypto</artifactId>
                            <version>${hutool.version}</version>
                        </artifact>
                        <moduleInfoSource>
                            module cn.hutool.crypto {
                            exports cn.hutool.crypto;
                            }
                        </moduleInfoSource>
                    </module>
                </modules>
            </configuration>
        </execution>
    </executions>
</plugin>

附录 II:jpackage 的参数

以下内容是在 MacOS 下的 GraalVM 21 运行 jpackage -h 显示的内容:

用法:jpackage <options>

示例用法:
--------------
    生成适合主机系统的应用程序包:
        对于模块化应用程序:
            jpackage -n name -p modulePath -m moduleName/className
        对于非模块化应用程序:
            jpackage -i inputDir -n name \
                --main-class className --main-jar myJar.jar
        从预构建的应用程序映像:
            jpackage -n name --app-image appImageDir
    生成应用程序映像:
        对于模块化应用程序:
            jpackage --type app-image -n name -p modulePath \
                -m moduleName/className
        对于非模块化应用程序:
            jpackage --type app-image -i inputDir -n name \
                --main-class className --main-jar myJar.jar
        要为 jlink 提供您自己的选项,请单独运行 jlink:
            jlink --output appRuntimeImage -p modulePath \
                --add-modules moduleName \
                --no-header-files [<additional jlink options>...]
            jpackage --type app-image -n name \
                -m moduleName/className --runtime-image appRuntimeImage
    生成 Java 运行时程序包:
        jpackage -n name --runtime-image <runtime-image>
    对预定义应用程序映像进行签名:
        jpackage --type app-image --app-image <app-image> \
            --mac-sign [<additional signing options>...]
        注:此模式下允许的其他选项只有:
              一组其他 mac 签名选项和 --verbose

一般选项:
  @<filename> 
          从文件读取选项和/或模式 
          可以多次使用此选项。
  --type -t <type> 
          要创建的程序包的类型
          有效值为:{"app-image", "dmg", "pkg"} 
          如果未指定此选项,则将创建与平台相关的
          默认类型。
  --app-version <version>
          应用程序和/或程序包的版本
  --copyright <copyright string>
          应用程序的版权
  --description <description string>
          应用程序的说明
  --help -h 
          将用法文本输出到输出流并退出,用法文本中包含
          适用于当前平台的每个有效选项的列表和说明
  --icon <file path>
          应用程序包图标的路径
          (绝对路径或相对于当前目录的路径)
  --name -n <name>
          应用程序和/或程序包的名称
  --dest -d <destination path>
          用来放置所生成的输出文件的路径
          (绝对路径或相对于当前目录的路径)
          默认为当前的工作目录。
  --temp <directory path>
          用来创建临时文件的新目录或空白目录的路径
          (绝对路径或相对于当前目录的路径)
          如果指定,则在任务完成时将不删除临时目录,
          必须手动删除临时目录。
          如果未指定,则将创建一个临时目录,
          并在任务完成时删除该临时目录。
  --vendor <vendor string>
          应用程序的供应商
  --verbose
          启用详细的输出
  --version
          将产品版本输出到输出流并退出。

用来创建运行时映像的选项:
  --add-modules <模块名称>[,<模块名称>...]
          要添加的模块的逗号 (",") 分隔列表
          此模块列表连同主模块(如果指定)
          将作为 --add-module 参数传递到 jlink。
          如果未指定,则仅使用主模块(如果指定了 --module),
          或者使用默认的模块集(如果指定了  
          --main-jar)。
          可以多次使用此选项。
  --module-path -p <module path>...
          路径的 : 分隔列表
          每个路径要么是模块的目录,要么是
          模块化 jar 的路径。
          (每个路径可以是绝对路径,也可以是相对于当前目录的路径。)
          可以多次使用此选项。
  --jlink-options <jlink 选项> 
          要传递给 jlink 的选项列表(用空格分隔) 
          如果未指定,则默认为 "--strip-native-commands 
          --strip-debug --no-man-pages --no-header-files"。 
          可以多次使用此选项。
  --runtime-image <directory path>
          将复制到应用程序映像的预定义
          运行时映像的路径
          (绝对路径或相对于当前目录的路径)
          如果未指定 --runtime-image,jpackage 将运行 jlink 以
          使用如下选项创建运行时映像:
          --strip-debug、--no-header-files、--no-man-pages 和 
          --strip-native-commands。

用来创建应用程序映像的选项:
  --input -i <directory path>
          包含要打包的文件的输入目录的路径
          (绝对路径或相对于当前目录的路径)
          输入目录中的所有文件将打包到
          应用程序映像中。
  --app-content <additional content>[,<additional content>...]
          要添加到应用程序有效负载中的文件和/或
          目录的逗号分隔路径列表。
          此选项可以多次使用。

用来创建应用程序启动程序的选项:
  --add-launcher <launcher name>=<file path>
          启动程序的名称和包含关键字-值对列表的
          属性文件的路径
          (绝对路径或相对于当前目录的路径)
          可以使用关键字 "module"、"main-jar"、"main-class"、"description"、
          "arguments"、"java-options"、"app-version"、"icon"、
          "launcher-as-service"、
          "win-console"、"win-shortcut"、"win-menu"、
          "linux-app-category" 和 "linux-shortcut"。
          这些选项将添加到原始命令行选项中或者用来覆盖
          原始命令行选项,以构建额外的替代启动程序。
          将从命令行选项构建主应用程序启动程序。
          可以使用此选项构建额外的替代启动程序,
          可以多次使用此选项来构建
          多个额外的启动程序。 
  --arguments <main class arguments>
          在没有为启动程序提供命令行参数时,
          要传递到主类的命令行参数
          可以多次使用此选项。
  --java-options <java options>
          要传递到 Java 运行时的选项
          可以多次使用此选项。
  --main-class <class name>
          要执行的应用程序主类的限定名称
          只有在指定了 --main-jar 时才能使用此选项。
  --main-jar <main jar file>
          应用程序的主 JAR;包含主类
          (指定为相对于输入路径的路径)
          可以指定 --module 或 --main-jar 选项,但是不能同时指定
          两者。
  --module -m <module name>[/<main class>]
          应用程序的主模块(以及可选的主类)
          此模块必须位于模块路径中。
          如果指定了此选项,则将在 Java 运行时映像中
          链接主模块。可以指定 --module 或 --main-jar 选项,
          但是不能同时指定这两个选项。
  --mac-package-identifier <ID string>
          用来唯一地标识 macOS 应用程序的标识符
          默认为主类名称。
          只能使用字母数字 (A-Z,a-z,0-9)、连字符 (-) 和
          句点 (.) 字符。
  --mac-package-name <name string>
          出现在菜单栏中的应用程序名称
          这可以与应用程序名称不同。
          此名称的长度必须小于 16 个字符,适合
          显示在菜单栏中和应用程序“信息”窗口中。
          默认为应用程序名称。
  --mac-package-signing-prefix <prefix string>
          在对应用程序包签名时,会在所有需要签名
          但当前没有程序包标识符的组件的
          前面加上此值。
  --mac-sign
          请求对程序包或预定义的应用程序映像
          进行签名。
  --mac-signing-keychain <keychain name>
          要用来搜索签名身份的密钥链的名称
          如果未指定,则使用标准的密钥链。
  --mac-signing-key-user-name <team name>
          Apple 签名身份的团队或用户名称部分。
  --mac-app-store
          指示 jpackage 输出面向
          Mac App Store。
  --mac-entitlements <file path>
          包含一些权利的文件的路径,在对捆绑包中的可执行文件
          和库进行签名时会使用这些权利。
  --mac-app-category <category string>
          用于构造应用程序 plist 中 LSApplicationCategoryType 的
          字符串。默认值为 "utilities"。

用来创建应用程序包的选项:
  --about-url <url>
          应用程序主页的 URL
  --app-image <directory path>
          用来构建可安装程序包的
          或对预定义应用程序映像进行签名的
          预定义应用程序映像的位置
          (绝对路径或相对于当前目录的路径)
  --file-associations <file path>
          包含关键字-值对列表的属性文件的路径
          (绝对路径或相对于当前目录的路径)
          可以使用关键字 "extension"、"mime-type"、"icon" 和 "description" 
          来描述此关联。
          可以多次使用此选项。
  --install-dir <directory path>
          应用程序安装目录的绝对路径
  --license-file <file path>
          许可证文件的路径
          (绝对路径或相对于当前目录的路径)
  --resource-dir <directory path>
          覆盖 jpackage 资源的路径
          可以通过向该目录中添加替代资源来覆盖 jpackage 的
          图标、模板文件和其他资源。
          (绝对路径或相对于当前目录的路径)
  --runtime-image <directory path>
          要安装的预定义运行时映像的路径
          (绝对路径或相对于当前目录的路径)
          在创建运行时程序包时需要使用选项。
  --launcher-as-service
          请求创建安装程序,以将主
          应用程序启动程序注册为后台服务类型应用程序。

用来创建应用程序包的与平台相关的选项:
  --mac-dmg-content <additional content path>[,<additional content path>...]
          包括 DMG 中引用的所有内容。
          此选项可以使用多次。