博文目录

文章目录

  • JavaFX 简单说明
    • JavaFX 版本
    • JavaFX 与 JDK 的关系
    • JavaFX 与 JDK Modular (JDK 9 模块化系统 module-info.java)
    • JavaFX 21 模块
    • JavaFX JMODs / SDK
      • JavaFX JMODs
      • JavaFX SDK
      • 区别
    • Scene Builder
      • 安装与配置
  • JavaFX 知识点
    • Java 模块化技术基础
    • JavaFX 基础编程模型
    • JavaFX MVC 架构
    • JavafX 事件处理机制
    • JavaFX 多窗体编程与数据传输
    • JavaFX 数据绑定机制
    • Java MVVM
    • JavaFX 多线程
    • JavaFX 集成第三方框架, 如 RxJava
    • JavaFX Chart 图表
  • 创建项目
    • pom.xml
    • module-info.java
    • 运行 HelloApplication
  • 编译与运行
    • 准备
    • 编译
    • 运行
  • 打包与分发
    • jlink
      • jlink 参数列表
        • `重要参数说明`
      • 生成最小 JRE
      • 生成包含 java.se 模块的 JRE
      • 生成包含 JavaFX 模块的 JRE
      • 生成包含自定义模块的 JRE
      • 生成包含主类启动器的 JRE
      • 限制
        • 测试
    • javafx:jlink
    • jpackage
      • wix v3
      • 打包为安装程序
      • 打包为运行程序
      • 限制
  • 无法使用 jlink 时打包分发
    • 打包可直接运行的 Jar
      • 修改项目

JavaFX 简单说明

JavaFX 官网
官网 Getting Started with JavaFX

JavaFX 是一个开源的下一代客户端应用程序平台,适用于基于 Java 构建的桌面、移动和嵌入式系统。它是许多个人和公司的协作成果,目标是为开发富客户端应用程序生成一个现代、高效且功能齐全的工具包。

JavaFX 主要致力于富客户端开发,以弥补 swing 的缺陷,主要提供图形库与 media 库,支持 audio,video,graphics,animation,3D 等,同时采用现代化的 css 方式支持界面设计。同时又采用 XUI 方式以 XML 方式设计 UI 界面,达到显示与逻辑的分离。与 android 这方面确实有点相似性。

JavaFX 版本

官网 版本说明

截止到 20240102, JavaFX 的版本如上图所示. JavaFX 11/17/21 是长期支持版本, 但 11 已到期不再更新, 建议选长期支持版本

JavaFX 与 JDK 的关系

JavaFX 建立在 JDK 之上,是一个独立的组件。从 JDK 11 开始, JavaFX 与 JDK 分开发布, JavaFX 不再集成于 JDK 中

JavaFX 与 JDK Modular (JDK 9 模块化系统 module-info.java)

在 Java 8 之后,JavaFX 从 JDK 中分离出来,然后在 Java 9 时,Java 引入了 Java 模块化系统。从那之后,JavaFX 要求使用 Java 模块化系统来运行 JavaFX。因此,当直接使用 Java 8 以上的环境运行非模块化 (无 module-info.java) 的 JavaFX 项目时就会出现如下报错。

错误: 缺少 JavaFX 运行时组件, 需要使用该组件来运行此应用程序

解决方法也有多种

  • 使用 JDK 8, 不建议, 不能从根源解决问题
  • 改造项目为模块化项目, 添加 module-info.java 模块申明文件
  • 在运行配置里添加 vm 参数 --module-path "javafx jmods (javafx.xxx.jmod) / sdk (javafx.xxx.jar) 路径" --add-modules javafx.controls,javafx.fxml, 相当于给 JDK 补充了 JavaFX 的模块, 如果路径里有空格, 则路径要加上双引号 ""
  • 使用引导类, 在另一个类的 main 方法中调用主类的 main 方法, 会自动生成一个匿名的模块系统, 程序可以运行, 但有警告提示
    1月 2, 2024 10:19:53 下午 com.sun.javafx.application.PlatformImpl startup警告: Unsupported JavaFX configuration: classes were loaded from 'unnamed module @491dbe27'

JavaFX 21 模块

官网 JavaFX 21 API 文档

  • javafx.base: 定义 JavaFX UI 工具包的基本 API,包括绑定、属性、集合和事件的 API。
  • javafx.controls: 定义可用于 JavaFX UI 工具包的 UI 控件、图表和外观。
  • javafx.fxml: 为 JavaFX UI 工具包定义 FXML API。
  • javafx.graphics: 为 JavaFX UI 工具包定义核心场景图 API(例如布局容器、应用程序生命周期、形状、转换、画布、输入、绘画、图像处理和效果),以​​及动画、CSS、并发、几何、打印的 API , 和开窗。
  • javafx.media: 定义用于播放媒体和音频内容的 API,作为 JavaFX UI 工具包的一部分,包括MediaView和 MediaPlayer.
  • javafx.swing: 为 JavaFX UI 工具包中包含的 JavaFX/Swing 互操作支持定义 API,包括SwingNode(用于将 Swing 嵌入 JavaFX 应用程序)和 JFXPanel(用于将 JavaFX 嵌入 Swing 应用程序)。
  • javafx.web: 为 JavaFX UI 工具包中包含的 WebView 功能定义 API。

JavaFX JMODs / SDK

官网 JavaFX SDK / JMODs 下载

在 Java 9 之前的版本中,JDK / JRE 包含了一个名为 rt.jar 的文件,它是 Java 核心库的一部分,包含了 Java 平台的类和资源。从 Java 9 开始,Java 平台引入了模块化系统,其中核心库被拆分为一组模块。rt.jar 被废弃,取而代之的是使用 JMOD 格式来打包和管理模块。

JavaFX JMODs

JavaFX SDK

区别

JEP 261: Module System

The new JMOD format goes beyond JAR files to include native code, configuration files, and other kinds of data that do not fit naturally, if at all, into JAR files.

JMOD files can be used at compile time and link time, but not at run time.

JMODs 可以用于编译 (javac) 和连接 (jlink), 但不能用于运行 (java)

Scene Builder

官网 Scene Builder 下载

Scene Builder 是针对 JavaFX FXML UI 的拖拽式页面设计编码工具, 免费且开源

当前版本是 Scene Builder 21.0.0, 运行需要 Java 17 and higher

如果使用 JDK 8 的 JavaFX, 可以下载 Scene Builder 8.5.0

安装与配置

官网 Scene Builder 下载

下载好 SceneBuilder 并运行, 选择合适的安装位置, 安装即可

在 Idea 里右键某个 fxml 文件, 选择使用 SceneBuilder 打开, 即可设置与 SceneBuilder.exe 的关联, 设置过后, 后续即可直接用 SceneBuilder 打开 fxml 文件, 编辑好后 Ctrl+S 保存即可直接作用于 fxml 文件


JavaFX 知识点

Java 模块化技术基础

51-Java模块化技术基础
廖雪峰 模块

  • module-info.java
  • –module-path: 依赖模块的查找目录
  • –add-modules: 指定除了默认模块额外要解析的模块
  • jmods: 用于生成 .jmod 格式的模块
  • jlink: 用于生成自定义 JRE 的工具, 可根据依赖自行精简 JRE, 便于打包和分发

JavaFX 基础编程模型

54-把握JavaFX编程模型

  • JavaFX 应用程序的主类需要继承自 javafx.application.Application 类, 然后重写其 start 方法, 此方法是所有 JavaFX 应用程序的入口点
  • JavaFX 应用程序将 UI 容器定义为 舞台(Stage)场景(Scene), Stage 是顶级容器, 它对应于窗体, 其内容有 Scene 决定. Scene 是所有可视化内容的 容器(Container). JavaFX 应用程序的可视化界面通常由 控件(Control)形状(Shape) 构成, 放到 Scene 中
  • JavaFX 中, Scene 中的内容会以由 图形节点(Node) 构成的分层 场景图(Scene Graph) 来展现. SceneGraph 其实就是一颗多叉树, 各种控件都是树中的节点, 最底层的节点通常是诸如按钮之类的控件, 也可以是 Circle 之类的图形. 拥有子树的节点称为容器, 在 JavaFX 中称为 布局(Layout)
  • Stage 与 Scene 是 1:1 的关系, Scene 与场景图的根节点是 1:1 的关系
  • JavaFX 应用程序的生命周期: initstartrunningstop, init / start / stop 由抽象类 javafx.application.Application 定义, 可以自定义覆盖
  • Stage 的生命周期: close / hide / show 等等, 可以给 Stage 挂接相关事件

JavaFX MVC 架构

55-JavaFX应用的MVC架构

  • MainClass: 程序的入口点, 通常包容管理多窗体, 实现各控制器之间相互通讯的相关代码
  • Model: 就是封装了数据的 JavaBean
  • View: 使用 FXML 编写, 包容可视化的 UI 控件, 使用户使用程序的媒介, 关联着一个控制器
  • Controller: 包容程序的应用逻辑, 负责响应用户操作, 负责在 View 和 Model 之间实现数据同步

JavafX 事件处理机制

56-把握JavaFX事件处理机制原理

  • 桌面应用都是 事件驱动 的, 事件(Event) 表示程序所感兴趣的某件事情发生了, 比如鼠标移动事件, 按键按下事件等
  • JavaFX 中, 事件是 javafx.event.Event 类或其任何子类的实例, JavaFX 提供了多种事件, 比如 DragEvent(拖动事件), KeyEvent, MouseEvent 等, JavaFX 提供的内置 UI 控件和图形对象, 都可以触发特定事件
  • JavaFX 中可以通过 Java 代码绑定事件, 也可以通过 FXML 绑定事件, 通过 FXML 声明的事件的方法要加 @FXML 注解
  • 事件派发链: 事件在控件树中的传播过程. 事件派发链是一个双向链表, 有时间目标对象负责创建, 事件发生时, 事件对象会在链中传播. 事件响应方法分为 EventFilter 和 EventHandler 两类, 先执行 EventFilter 事件链
    • EventFilter: 下沉: 从根节点向下来到事件发生节点
    • EventHandler: 冒泡: 从事件发生节点向上去往根节点
    • 举例: HBox 里放了个 Label: 两者都分别挂载了两个方向的 MOUSE_PRESSED 事件, 如果在标签上点击左键, 则会按照先 EventFilter 再 EventHandler 的顺序来调用响应方法, Filter 的先执行 hBox 的再执行 label 的, Handler 的先执行 label 的再执行 hBox 的
      • hBox.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {})
      • hBox.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {})
      • label.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {})
      • label.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {})

JavaFX 多窗体编程与数据传输

57-JavaFX多窗体编程

  • 每个 Stage 对应一个窗体
  • 模态窗体: 具有父子关系且子窗体出现时, 父窗体不能操作
  • 对象互相传递消息: 就是对象互相持有引用, 调用对方的方法来传递数据, 在 JavaFX 中窗体间传递信息靠的就是各 Controller 相互引用, 调用回调函数

JavaFX 数据绑定机制

58-JavaFX数据绑定机制及应用

  • JavaFX Bean Property: 提供了很多 SimpleIntegerProperty 之类的工具类, 可以绑定值改变事件, 代码操作值, 绑定的 UI 自动跟新
  • JavaFX 数据绑定编程模式
    • 绑定数据源: 具备改变通知能力的数据对象与数据集合, 包括 JavaFX Bean Property / ObservableValue / ObservableList
    • JavaFX 应用程序的 UI 界面: 包容 TableView / Label 等支持数据绑定的控件
    • JavaFX 数据绑定机制: Binding 对象
    • 举例: Circle 始终居于窗体中央, Circle 自带圆心横纵坐标的 Property 属性, Scene 自带宽度高度的 Property 属性
      • circle.centerXProperty().bind(Bindings.divide(scene.widthProperty(), 2));
      • circle.centerYProperty().bind(Bindings.divide(scene.heightProperty(), 2));
  • 优点: 业务逻辑与 UI 分离, 绑定好关系之后, 属性值变化, UI 会自动更新, 无需代码修改 UI
  • 双向绑定:
    • 举例: 两个 TextField 保持内容一致, 再任何一方修改, 另一方保持内容相同
      • repeater.textProperty().bindBidirectional(speaker.textProperty());

Java MVVM

59-JavaFX实现MVVM架构

  • View: 还是 FXML 定义的 UI
  • ViewModel: 和 MVC 中的 Model 一样, 但是使用 JavaFX Bean Property / ObservableValue / ObservableList 具备值变化通知能力的工具来做成员属性, 其与 UI 上的某些控件的值相对应
  • Controller: 实现 javafx.fxml.Initializable 接口, 在其提供的 initialize 方法中做如下事情
    • 控件的事件挂载
    • ViewModel 和 UI 控件做双向数据绑定, 这样 Controller 直接操作 ViewModel 的属性即可同步到 UI, 在 UI 相关控件上修改值也能同步到 ViewModel 的相关字段中
    • 注意: UI 变化完全由数据绑定后自行更新, Controller 不做任何 UI 的控制
  • 举例: 登录功能, UI 有 username 和 password 两个 TextField, login 和 cancel 两个 Button
    • LoginApplication: 程序主类入口
    • LoginViewModel: 持有 usernameProperty 和 passwordProperty 两个 StringProperty, 还有相关的 getter / setter / protertyGetter
    • LoginController: 实现 login 和 cancel 两个方法, 在 initialize 方法中做 login 按钮和 login 方法的绑定, cancel 按钮和 cancel 方法的绑定, ViewModel 中 usernameProperty 和 username 文本框的双向绑定, ViewModel 中 passwordProperty 和 password 文本框的双向绑定
      • login: 通过获取并验证 ViewModel 中 usernameProperty 和 passwordProperty 的值做合适的逻辑处理
      • cancel: 清空 ViewModel 中 usernameProperty 和 passwordProperty 的值
  • 也可以把 login 中的业务逻辑放到其他方法或类中, 这样 单元测试 时可以只测业务不看 UI

JavaFX 多线程

60-JavaFX多线程及典型示例展示

  • UI 操作有专门的 JavaFX Application Thread 线程负责, 自行创建的线程不允许操作 UI
  • 主线程中不要直接调用耗时很久的方法, 会导致窗体冻结. 要用一个线程去这些任务, 如果任务完成后需要更新 UI, 则可以使用 Platform.runLater(() - > {}) 的方式, 这里 Lambda 表达式执行的操作会被推送到 JavaFX Application Thread 线程中执行, 所以可以访问 UI 控件
  • JavaFX 的 Worker 和 Task: 提供了后台线程运行并更新 UI 的相关功能封装, 支持中途取消

JavaFX 集成第三方框架, 如 RxJava

  • RxJava: 一款响应式的框架
  • 需要注意一点, 需要更新 UI 的地方用 Platform.runLater

JavaFX Chart 图表

  • JavaFX 内置了 图表 相关控件, 支持数据绑定, 可做一些数据可视化相关功能

创建项目

通过 Idea (2023.3.2) 创建时选 JavaFX 项目, 使用 JDK 21 和 JavaFX 21, 无需勾选可选的依赖库. 默认工程结构如下, 默认就是 Modular (存在 module-info.java) 的

pom.xml

默认 pom 内容如下, 可根据实际情况修改, 比如 javafx-controls 和 javafx-fxml 的版本和 javafx-maven-plugin 插件的配置等

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.coder</groupId><artifactId>demo</artifactId><version>1.0-SNAPSHOT</version><name>demo</name><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><junit.version>5.10.0</junit.version></properties><dependencies><dependency><groupId>org.openjfx</groupId><artifactId>javafx-controls</artifactId><version>21</version></dependency><dependency><groupId>org.openjfx</groupId><artifactId>javafx-fxml</artifactId><version>21</version></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>${junit.version}</version><scope>test</scope></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><version>${junit.version}</version><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.11.0</version><configuration><source>21</source><target>21</target></configuration></plugin><plugin><groupId>org.openjfx</groupId><artifactId>javafx-maven-plugin</artifactId><version>0.0.8</version><executions><execution><id>default-cli</id><configuration><mainClass>com.coder.demo/com.coder.demo.HelloApplication</mainClass><launcher>app</launcher><jlinkZipName>app</jlinkZipName><jlinkImageName>app</jlinkImageName><noManPages>true</noManPages><stripDebug>true</stripDebug><noHeaderFiles>true</noHeaderFiles></configuration></execution></executions></plugin></plugins></build></project>

module-info.java

module com.coder.demo {requires javafx.controls;requires javafx.fxml;opens com.coder.demo to javafx.fxml;exports com.coder.demo;}

运行 HelloApplication

编译与运行

通过 Idea 可以便捷编译与运行, 直接点击 运行 按钮即可, 下面讲述通过 命令行 运行, 便于理解编译和运行的过程

该工程非常简单, 如上图, src 是源码文件夹, 采用命令行的方式编译只需要 src 即可

准备

官网 JavaFX SDK / JMODs 下载

编译该工程需要依赖 JavaFX 的 javafx.controls 和 javafx.fxml 两个模块, 使用 SDK 或者 JMODs 都可以, 以下是本机的依赖路径

  • SDK: C:\mrathena\develop\javafx-sdk-21.0.1\lib, 注意需要精确到 lib 文件夹, 其内是多个 jar 文件
  • JMODs: C:\mrathena\develop\javafx-jmods-21.0.1, 其内是多个 jmod 文件

编译

编译需要使用对应版本的 JDK 21, 需要使用 --module-path 指定查找模块的路径

# 切换工作目录cd C:\mrathena\develop\workspace\idea\mrathena\demo
# 编译 (使用 SDK)dir /s /b src\*.java > sources.txt & javac --module-path C:\mrathena\develop\javafx-sdk-21.0.1\lib -d mods/javafx @sources.txt & del sources.txt# 编译 (使用 JMODs)dir /s /b src\*.java > sources.txt & javac --module-path C:\mrathena\develop\javafx-jmods-21.0.1 -d mods/javafx @sources.txt & del sources.txt
# 指令说明# &: 将多个操作连到一起, 写成一行命令# 第一部分# dir: 列出当前目录下的所有文件夹和文件, 包含详细信息# dir /s: 列出当前目录及其所有子目录下的所有文件和文件夹, 包含详细信息# dir /b: 列出当前目录下的所有文件夹和文件, 只有文件名, 不包含其他信息# dir /s /b: 列出当前目录及其所有子目录下的所有文件和文件夹的完整路径# dir /s /b src\*.java: 列出当前目录下的 src 文件夹及其所有子文件夹下的 java 文件的完整路径# > sources.txt: 将列出的 java 文件的完整路径保存到 sources.txt 文件中dir /s /b src\*.java > sources.txt# 第二部分# javac: java 编译工具# javac -help: 查看 javac 的选项# 用法: javac  # @: 从文件读取选项和文件名, 如下方的 @sources.txt, 就是要从该文件中读取 javac 命令需要的 选项 和 源文件名称# --module-path , -p : 指定查找应用程序模块的位置(依赖的模块的位置), 类似之前的 -classpath 指定依赖的位置# -d : 指定放置生成的类文件的位置# 指定 JavaFX SDK 下的 lib 为依赖模块的位置, 指定将生成的 class 文件存放在当前目录下的 mods/javafx 中, 指定源文件从 sources.txt 中读取javac --module-path C:\mrathena\develop\javafx-sdk-21.0.1\lib -d mods/javafx @sources.txt# 第三部分# 最后删掉生成的 sources.txt 文件del sources.txt

注意: 编译生成的 mods/javafx 只是一个存放编译好的模块的文件夹而已, 可以是任意位置, 在 javafx 文件夹下的模块的名称由其中的 module-info.class 定义, 和文件夹名称没有关系. 本工程中我们定义的模块名称是 com.coder.demo, 而不是 javafx

运行

# 运行 (只能使用 SDK, 不能使用 JMODs)java --module-path C:\mrathena\develop\javafx-sdk-21.0.1\lib;mods --module com.coder.demo/com.coder.demo.HelloApplication
# java: java 运行工具# 用法: java [options]  [args...]: 执行类# 用法: java [options] -jar  [args...]: 执行 jar 文件# 用法: java [options] -m [/] [args...]: 执行模块中的主类# 用法: java [options] --module [/] [args...]: 执行模块中的主类# 用法: java [options]  [args]: 执行单个源文件程序. 现在能直接执行源文件了" />FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));

也可以使用 javafx-maven-plugin 插件提供的 mvn 命令运行

mvn clean javafx:run

打包与分发

jlink

使用 jlink 可以定制化 JRE, 可以做到如下的一些事情

  • 可以排除一些没有使用到的模块, 缩小 JRE 的体积以便于分发
  • 可以将 JDK 和 JavaFX 的相关模块打包到一起便于 JavaFX 应用程序的运行
  • 可以把我们自己自定义的模块也打包进去, 便于自定义模块的运行

jlink 参数列表

jlink 只能使用 JavaFX JMODs 而不能使用 JavaFX SDK

jlink --help用法: jlink  --module-path [;...] --add-modules [,...]可能的选项包括:--add-modules [,...]除了初始模块之外要解析的根模块。 还可以为 ALL-MODULE-PATH。--bind-services 链接服务提供方模块及其被依赖对象-c, --compress=Enable compression of resources:Level 0: No compressionLevel 1: Constant string sharingLevel 2: ZIP--disable-plugin  Disable the plugin mentioned--endian  所生成 jimage 的字节顺序 (默认值: native)-h, --help, -?输出此帮助消息--ignore-signing-information在映像中链接已签名模块化 JAR 的情况下隐藏致命错误。已签名模块化 JAR 的签名相关文件将不会复制到运行时映像。--launcher =[/]为模块和主类添加给定名称的启动程序命令 (如果指定)--limit-modules [,...]限制可观察模块的领域--list-pluginsList available plugins-p, --module-path 模块路径。如果未指定,将使用 JDK 的 jmods 目录(如果存在该目录)。如果指定,但它不包含 java.base 模块,则将添加 JDK 的 jmods 目录(如果存在该目录)。--no-header-files Exclude include header files--no-man-pagesExclude man pages--output 输出路径的位置--save-opts 将 jlink 选项保存在指定文件中-G, --strip-debug Strip debug information--suggest-providers [,...]建议可从模块路径中实现给定服务类型的提供方-v, --verbose 启用详细跟踪--version版本信息@从文件中读取选项
重要参数说明
  • --module-path: 依赖模块的查找路径, 默认值是 JDK 的 jmods 目录, 如果指定了该选项, 但目录中不包含 java.base 模块, 将会自动添加 JDK 的 jmods 目录
  • --add-modules: 将指定的模块及其可以传递到的依赖模块都打包到新的 JRE 中

生成最小 JRE

java.base 模块是 JDK 中最基础的模块, 是唯一一个没有引用其他任何模块的模块, 该模块暴露出了 JavaSE 的核心工具, 如java.lang / java.io / java.math / java.text / java.time / java.util 等

# 生成只包含 java.base 模块的 JREjlink --add-modules java.base --output jre
# 查看该 JRE, 只有一个 java.base 模块jre\bin\java --list-modulesjava.base@21.0.1

看了一下还有 40MB 大小

生成包含 java.se 模块的 JRE

java.se 模块是一个聚合模块, 该模块没有任何代码, 只有一个 module-info.class 文件, 用于声明一些依赖, 产生聚合的作用, 达到引用该模块就相当于引用很多其他模块的效果

不建议直接引用 java.se 模块,因为它就相当于 Java 9 以前版本的 rt.jar 的内容

# 检查 java.se 中的模块java --describe-module java.sejava.se@21.0.1requires java.datatransfer transitiverequires java.naming transitiverequires java.management transitiverequires java.base mandatedrequires java.transaction.xa transitiverequires java.instrument transitiverequires java.sql.rowset transitiverequires java.management.rmi transitiverequires java.sql transitiverequires java.compiler transitiverequires java.scripting transitiverequires java.security.sasl transitiverequires java.prefs transitiverequires java.security.jgss transitiverequires java.xml transitiverequires java.desktop transitiverequires java.rmi transitiverequires java.net.http transitiverequires java.logging transitiverequires java.xml.crypto transitive
# 生成包含 java.se 所依赖的全部模块的 JREjlink --add-modules java.se --output jre
# 查看该 JRE 中包含的模块jre\bin\java --list-modulesjava.base@21.0.1java.compiler@21.0.1java.datatransfer@21.0.1java.desktop@21.0.1java.instrument@21.0.1java.logging@21.0.1java.management@21.0.1java.management.rmi@21.0.1java.naming@21.0.1java.net.http@21.0.1java.prefs@21.0.1java.rmi@21.0.1java.scripting@21.0.1java.se@21.0.1java.security.jgss@21.0.1java.security.sasl@21.0.1java.sql@21.0.1java.sql.rowset@21.0.1java.transaction.xa@21.0.1java.xml@21.0.1java.xml.crypto@21.0.1

看了一下还有 88MB 大小

生成包含 JavaFX 模块的 JRE

官网 JavaFX SDK / JMODs 下载

# 检查 JavaFX 模块的声明java --module-path C:\mrathena\develop\javafx-sdk-21.0.1\lib --describe-module javafx.basejava --module-path C:\mrathena\develop\javafx-sdk-21.0.1\lib --describe-module javafx.controls# ...
# 生成包含 java.se 和 JavaFX 全部模块的 JREjlink --module-path C:\mrathena\develop\javafx-jmods-21.0.1 --add-modules java.se,javafx.base,javafx.controls,javafx.fxml,javafx.graphics,javafx.media,javafx.swing,javafx.web --output jre# 根据依赖传递申明, 可简写为jlink --module-path C:\mrathena\develop\javafx-jmods-21.0.1 --add-modules java.se,javafx.web,javafx.swing,javafx.fxml --output jre
# 查看该 JRE 中包含的模块jre\bin\java --list-modulesjava.base@21.0.1java.compiler@21.0.1java.datatransfer@21.0.1java.desktop@21.0.1java.instrument@21.0.1java.logging@21.0.1java.management@21.0.1java.management.rmi@21.0.1java.naming@21.0.1java.net.http@21.0.1java.prefs@21.0.1java.rmi@21.0.1java.scripting@21.0.1java.se@21.0.1java.security.jgss@21.0.1java.security.sasl@21.0.1java.sql@21.0.1java.sql.rowset@21.0.1java.transaction.xa@21.0.1java.xml@21.0.1java.xml.crypto@21.0.1javafx.base@21.0.1javafx.controls@21.0.1javafx.fxml@21.0.1javafx.graphics@21.0.1javafx.media@21.0.1javafx.swing@21.0.1javafx.web@21.0.1jdk.jsobject@21.0.1jdk.unsupported@21.0.1jdk.unsupported.desktop@21.0.1jdk.xml.dom@21.0.1

通过这种方式生成的 JRE, 已经包含了 JavaFX 相关依赖模块, 可以用来直接运行 JavaFX 应用程序, 可以分发给其他用户, 而其他用户不需要再下载 JDK / JRE / JavaFX

# 运行, 不再需要 --module-path 指向 JavaFX 依赖jre\bin\java --module-path mods --module com.coder.demo/com.coder.demo.HelloApplication

生成包含自定义模块的 JRE

# 打包 JRE, 根据依赖传递申明, com.coder.demo 依赖的 javafx.controls 和 javafx.fxml 模块也会被一并打包jlink --module-path C:\mrathena\develop\javafx-jmods-21.0.1;mods --add-modules com.coder.demo --output jre

通过这种方式生成的 JRE, 已经包含了主类运行所需要的所有模块, 运行主类不再需要 –module-path

# 运行, 不再需要 --module-pathjre\bin\java --module com.coder.demo/com.coder.demo.HelloApplication

生成包含主类启动器的 JRE

# 打包 JRE 并生成主类启动器, 在 JRE 的 bin 下生成一个 run 指令, 用于启动指定的主类jlink --module-path C:\mrathena\develop\javafx-jmods-21.0.1;mods --add-modules com.coder.demo --output jre --launcher run=com.coder.demo/com.coder.demo.HelloApplication

通过这种方式生成的 JRE, 已经包含了主类运行所需要的所有模块, 同时包含了运行主类的快捷方式, 运行主类不再需要 –module

# 运行, 不再需要指定主类jre\bin\run

JRE 中集成了我们的自定义模块功能, 将该 JRE 发给别人, 即可直接运行程序

限制

使用 jlink 工具打包自定义模块和依赖模块成为自定义运行时, 要求所有依赖必须是 Named Module (具名模块), 即必须有 module-info.class, 而 Unamed Module (匿名模块) 或者 Automatic Module (自动模块, 在 MANIFEST.MF 文件中通过 Automatic-Module-Name 指定模块名称) 都不能被打包

所以要想使用 jlink 工具, 就需要将工程里面的非模块化的依赖, 比如 OkHttp3 使用同类模块化工具替换掉, 比如 Java 11 引入的 Http Client

测试

这里使用自制的一个自动模块 toolkit (Jar 中没有 module-info.class, 通过 MANIFEST.MF 文件中的 Automatic-Module-Name 指定了模块名称为 toolkit), 里面有一个 Toolkit 工具类, 只有一个返回 String 的静态方法, 在项目中执行该工具代码, 以此来测试编译运行与打包

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.coder</groupId><artifactId>toolkit</artifactId><version>1.0.0</version><properties><maven.compiler.source>21</maven.compiler.source><maven.compiler.target>21</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.3.0</version><configuration><archive><manifestEntries><Automatic-Module-Name>toolkit</Automatic-Module-Name></manifestEntries></archive></configuration></plugin></plugins></build></project>
# 编译, 需要将新增的依赖加入到模块路径, 就是 Maven 本地仓库中的依赖目录dir /s /b src\*.java > sources.txt & javac --module-path C:\coder\develop\javafx-jmods-21.0.1;C:\coder\resource\develop\maven\repository\com\coder\toolkit\1.0.0 -d mods/javafx @sources.txt & del sources.txt
# 运行, 需要将新增的依赖加入到模块路径, 就是 Maven 本地仓库中的依赖目录java --module-path C:\coder\develop\javafx-sdk-21.0.1\lib;C:\coder\resource\develop\maven\repository\com\coder\toolkit\1.0.0;mods --module com.coder.demo/com.coder.demo.HelloApplication
# jlink, 需要将新增的依赖加入到模块路径, 就是 Maven 本地仓库中的依赖目录jlink --module-path C:\coder\develop\javafx-jmods-21.0.1;C:\coder\resource\develop\maven\repository\com\coder\toolkit\1.0.0;mods --add-modules com.coder.demo --output jre --launcher run=com.coder.demo/com.coder.demo.HelloApplication

编译和运行都可以正常执行, 但是连接会报错

错误: 自动模块不能用于来自 file:///C:/coder/resource/develop/maven/repository/com/coder/toolkit/1.0.0/toolkit-1.0.0.jar 的 jlink: toolkit

javafx:jlink

运行 javafx-maven-plugin 提供的 javafx:jlink 命令, 可以按照插件配置生成运行时, 包含所有项目功能, 且内置主类启动程序, 能自动配置正确的模块目录和模块名称等, 比手动执行 jlink 方便很多. 但和 jlink 有同样的限制

jpackage

Java 14 提供的打包工具, 可以将程序打包成 exe

单独看 jpackage 可能不太好理解, 但是如果是顺着上文下来的, 那么自然而然就会 jpackage 打包了, 一脉相承

  • 首先通过 jpackage --help 查看选项列表说明, 官方已经给出了针对多种情况的简单命令行案例, 而且把各种参数已经做好了分类
  • 几个关键名词和参数
    • application package suitable: 就是安装程序包, 双击后是一套安装程序, 安装好后才能在指定位置找到 exe 运行程序
    • application image: 就是运行程序包, 双击直接运行, 无需安装的那种
    • –name: 打包出的 exe 的名称, 生成的存放 exe 的文件夹也是这个名称
    • –type: 打包的类型, Windows 平台支持 {“app-image”, “exe”, “msi”}, 只有 app-image 是打包为可执行程序, 其他都是安装包程序
    • –input: 指定一个资源文件夹, 该文件夹下的所有内容都会被打包
      • 我目前只有在打包非模块化工程时, 通过该参数指定 jar 包所在的路径, 目录下也只有一个可运行的 jar 包, 暂没感觉到其他用处
    • –dest: 输出路径, 默认是当前所在工作路径, 该路径与 --input 指定的路径 一定不能相同, 会生成超深文件夹, 无法直接删除(可递归从里向外删除)
    • –icon: 指定可执行程序的图标
    • –module-path: 默认包含 JDK 的 jmods 目录, 额外补充依赖模块的查找路径, 可以是模块的目录, 也可以是模块化的 jar 的路径, 用 ; 分割
    • –module: 指定应用程序的主模块(和主类), 必须在模块查找路径所覆盖的范围内, 一旦指定该选项, 主模块将会链接到运行时?? 我觉得应该是自动生成合适的 JRE 的意思
    • –main-jar:
      • 非模块化的 Jar 打包为 exe 时需要该参数, 同时 –input 参数必须存在, 且该 jar 必须在 –input 指定的目录下
      • 模块化的 Jar 也可以用该参数打包 exe, 但是没必要, 直接用编译好的模块 class 打包就好了, 无需转成 Jar 再打包. 我猜的
    • –main-class
      • 如果 jar 是可直接运行的 jar, 则可以不使用该参数指定运行的主类. 也可以使用该参数将运行主类指向其他可运行类
      • 如果 jar 是不可直接运行的, 但是包含可运行主类, 可通过该参数指定
    • 其他 win 参数: … 自行查看 help 的解释

wix v3

使用 jpackage 需要安装 WIX v3 或更高版本, 官网, GitHub, 安装完貌似不需要配置环境变量即可直接使用

打包为安装程序

jpackage --name demo --module-path C:\mrathena\develop\javafx-jmods-21.0.1;mods --module com.coder.demo/com.coder.demo.HelloApplication

生成一个 demo-1.0.exe 的安装程序, 安装完后自带 JRE, 默认安装路径在 C:\Program Files 下, 双击 demo.exe 可运行, 可通过其他参数配置安装路径, 快捷方式等


在 控制面板-卸载程序 中可以看到安装的该程序, 可正常卸载

打包为运行程序

# 当前目录生成 demo 文件夹, 内含自动生成的 JREjpackage --module-path C:\mrathena\develop\javafx-jmods-21.0.1;mods --module com.coder.demo/com.coder.demo.HelloApplication --type app-image --name demo
# 使用自定义的 JRE, 输出到当前目录, 参数根据 jre 不同有所区别, 比如 --module-path 和 --module 等可能不需要写jpackage --module-path C:\mrathena\develop\javafx-jmods-21.0.1;mods --module com.coder.demo/com.coder.demo.HelloApplicatio --runtime-image jre --type app-image --name demo
# 自动生成 JRE, 自定义图标, 输出到桌面jpackage --module-path C:\mrathena\develop\javafx-jmods-21.0.1;mods --module com.coder.demo/com.coder.demo.HelloApplication --type app-image --name demo --icon D:\resource\头像.狐狸.ico --dest C:\Users\mrathena\Desktop

双击 demo.exe 即可直接运行

限制

jpackage 如果配置自动生成运行时, 则内部会调用 jlink, 拥有相同的限制

# jpackage, 需要将新增的依赖加入到模块路径, 就是 Maven 本地仓库中的依赖目录jpackage --module-path C:\coder\develop\javafx-sdk-21.0.1\lib;C:\coder\resource\develop\maven\repository\com\coder\toolkit\1.0.0;mods --module com.coder.demo/com.coder.demo.HelloApplication --type app-image --name demo --icon C:\coder\resource\图片\头像.狐狸.ico --dest C:\Users\coder\Desktop
C:\coder\develop\workspace\idea\mrathena\demo>jpackage --module-path C:\coder\develop\javafx-sdk-21.0.1\lib;C:\coder\resource\develop\maven\repository\com\coder\toolkit\1.0.0;mods --module com.coder.demo/com.coder.demo.HelloApplication --type app-image --name demo --icon C:\coder\resource\图片\头像.狐狸.ico --dest C:\Users\coder\Desktopjlink 失败,出现 错误: 自动模块不能用于来自 file:///C:/coder/resource/develop/maven/repository/com/coder/toolkit/1.0.0/toolkit-1.0.0.jar 的 jlink: toolkit

无法使用 jlink 时打包分发

有时候, 项目依赖的非具名模块无法被替换, 导致工程无法使用 jlink, 亦无法直接使用 jpackage. 但可以通过 jpackage 的 –main-jar 参数来指定可运行 jar 的方式替代, 这就需要先将项目打包成为一个可直接运行的 Jar

还是以上面 jlink 限制中举的例子为例来尝试打包

打包可直接运行的 Jar

修改项目

添加一个新的入口主类, 内部调用 HelloApplication.main

package com.coder.demo;public class Application {public static void main(String[] args) {HelloApplication.main(args);}}

修改 pom.xml 文件, 添加 maven-jar-plugin 和 maven-dependency-plugin 以及对应配置, 注意 maven-jar-plugin 的 mainClass 指向新添加的入口主类, 这里不要用 模块\主类 的写法, 而是直接写主类即可

<finalName>demo</finalName><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.3.0</version><configuration><archive><manifest><addClasspath>true</addClasspath><classpathPrefix>libs/</classpathPrefix><mainClass>com.coder.demo.Application</mainClass></manifest></archive></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.6.1</version><executions><execution><id>copy-dependencies</id><phase>prepare-package</phase><goals><goal>copy-dependencies</goal></goals><configuration><outputDirectory>${project.build.directory}/libs</outputDirectory></configuration></execution></executions></plugin>

然后运行 Maven 的 clean 和 package 命令, 在 target 中会生成 demo.jar 和 libs 依赖文件夹, 通过 java -jar 运行 demo.jar 验证其可执行

在 target 中新建一个文件夹 test, 将 demo.jar 和 libs 剪切到 test 中, 执行下面的 jpackage 命令, 在桌面生成 exe

jpackage --type app-image --name demo --icon C:\coder\resource\图片\头像.狐狸.ico --input C:\coder\develop\workspace\idea\mrathena\demo\target\test --main-jar demo.jar --dest C:\Users\coder\Desktop# --input 参数指定打包的资源为刚刚新建的 test 文件夹的绝对路径# --main-jar 指定可执行 jar 文件, 即 demo.jar# --main-class 可通过该参数强行改变可执行 jar 启动时的运行主类

有一点非常重要, 就是默认输出目录就是当前目录, 该目录一定不能和参数 --input 指定的打包目录相同(该目录下的所有文件都会被打包), 建议显式指定 --desc 参数并指向不同位置. 不然生成出的内容会不断被尝试打包, 最终因为文件夹深度太深而报错或路径超过字符数限制而报错, 关键还无法直接删除, 需要写代码递归从最深处往外删, 我不小心生成了一个 2000 多层的文件夹目录, 最终靠写递归代码才删掉

生成物目录结构如下, 点击 demo.exe 可正常运行


另外, 其他的可执行 Jar 也可以通过这种方式打包成 exe, 比如用 java.swing 写的工具的可执行 jar