☰
JDK17+JavaFX+jpackage:开发Java桌面应用并打包exe指南
2026/10/1 23:13:14 网站建设 项目流程

如果你写过几年 Java,平时主要跑后端服务,突然要做一个带界面的内网小工具,第一反应大概率是“忍一忍用 Swing”。结果拖完控件一看,那画风真的有点倒胃口。后来我改用了 JavaFX,配合 IDEA、JDK17 和 OpenJFX,做过批量文件处理、内部数据看板、简易可视化导入导出工具,甚至帮同事封装过一个带界面的日志分析器。这套流程最大的价值在于:从建项目到打出 exe,最快一次不到四十分钟。这篇文章我就把这套完整流程拆开讲,包括环境配置、应用开发、依赖处理,以及最后怎么用 JDK 自带的 jpackage,把 JavaFX 应用打包成 Windows 下双击就能跑的 exe。整个过程没有商用框架、不写一行 C++、也不依赖外网工具,适合给内部小团队做工具分发、个人效率软件,或者大学生交课程设计。

1. 项目设计与技术选型:为什么是 JDK17 + OpenJFX

1.1 JavaFX 和 Swing,到底差在哪

很多人对 Java 桌面开发的印象还停留在 Swing 时代,那确实很劝退。JavaFX 是 Oracle 推出的现代化 UI 框架,它的定位就是让 Java 开发者能写出一套像样的桌面应用,而不只是“能弹个窗口”。JavaFX 设计上更接近 WPF 和 QML 的思路,界面用 CSS 做样式,布局有 BorderPane、GridPane 这类容器,控件也丰富得多,还内置了动画、图表、并发工具,做一个带交互的业务工具完全够用。

到了 JDK 11,Oracle 把 JavaFX 从 JDK 中剥离出来,改成由 OpenJFX 开源项目单独维护,版本号跟 JDK 保持同步。也就是说,现在想要在 JDK17 上用 JavaFX,你得额外引入 OpenJFX 的依赖。这一步看着多了一个环节,实际上反而是好事。一来 JavaFX 不再受 JDK 完整发布节奏的拖累,一年能出多个小版本,修复速度更快;二来通过 Maven 或 Gradle 引入依赖,版本干净可控,不会出现别人电脑上预装了一个老 JDK 导致 JavaFX 版本被捆绑的尴尬。

1.2 JDK17 在这个组合里的地位

JDK17 是 2021 年发布的 LTS 长期支持版本,技术圈对这个版本认可度很高。原因有几个:它引入了 sealed class、扩展了 switch 表达式、支持了 pattern matching for instanceof,这些语法让代码写起来更紧凑;Record 类在 16 转正,JavaFX 项目的实体类写起来舒服很多;更重要的是,长期支持意味着你的工具以后要给别人用,对方不需要纠结频繁升级 JDK 的问题。

还有人会问:都 2024 年了,JDK21 都出来了,为什么还要用 17?我的理由是,17 的生态最稳。各种 IDE 插件、构建工具、第三方库对 17 的支持最成熟,遇到问题搜解决方案也最容易。JavaFX 17 是 OpenJFX 的 LTS 版本,两者配合不用考虑版本兼容问题。除非你项目有特殊需求,否则 17 就是桌面小工具最省心的选择。

1.3 打包 exe 的三条路线,为什么选了 jpackage

做桌面应用的人都知道,写界面只完成了一半,另一半是怎么把程序发给别人。Java 项目的 exe 打包方案,常见的就三种:jpackage、Launch4j、GraalVM Native Image。三者也有人对比过热词,我直接说一下实际体验。

Launch4j 是给 jar 包套了一层 Windows exe 外壳,真正跑起来的时候仍然依赖目标机器上有没有 JRE,如果对方电脑没装 Java,程序直接启动不了。这不是真正的“免环境”分发。

GraalVM Native Image 能把 Java 应用编译成原生可执行文件,启动速度确实快,体积也小,但 JavaFX 对它支持不算全,一些控件库、反射场景、动态代理很容易踩坑。而且 Native Image 编译时间长、内存要求高,做一个内部小工具很不划算。

jpackage 是 JDK 自带的打包工具,JDK14 引入,JDK16 开始转正,专门用于构建自包含应用。它能把 JRE、JavaFX 模块、你的应用代码一起打包,生成 exe 安装包,目标机器不需要安装任何 Java 环境。同时 jpackage 还支持生成免安装的 app-image 目录,拷给谁都能直接跑。对我这种追求“快速交付”的人来说,这条路线最省事。

1.4 为什么不建议手动下 jar 包

网上很多老教程是让你去 OpenJFX 官网下载 SDK,然后把 lib 目录里的 jar 包手动加进 IDEA 的 Project Structure。这个做法能跑,但问题在后头:JavaFX 的 native 库是按平台拆分的,Windows 下的 jar 包和 Linux 下的不一样;依赖传递关系还牵扯 javafx-base、javafx-graphics,你漏一个就起动不了。而且将来升级版本,手动删包换包非常容易出错。

所以我强烈建议用 Maven 管理依赖。只需要在 pom.xml 里声明一个 javafx-controls,Maven 会自动把 javafx-graphics、javafx-base 一起拉下来,版本统一、干净明了,后面打包时也能用 Maven 插件统一处理依赖目录。

2. 环境准备与项目初始化:从 JDK17 到 IDEA 第一家窗口

2.1 JDK17 安装与环境变量配置

开始之前,先确认你机器上有 JDK17。下载地址不写了,直接去 Oracle 官网或 Adoptium(Eclipse Temurin)下载 Windows x64 的 zip 或安装版都行。我个人习惯用 zip 解压版,因为不污染注册表,换版本也方便。

安装完后做两件事:一是设置系统环境变量JAVA_HOME,指向你的 JDK 解压目录,比如D:\soft\jdk-17;二是在Path变量里增加%JAVA_HOME%\bin。注意啊,Path里要放在比较靠前的位置,因为很多电脑上可能还有 Python、MySQL 自带的其他 Java,变量顺序会影响最终生效版本。

验证是否成功,打开命令提示符输入:

java -version javac -version

如果显示的是 17 开头,就说明环境变量配好了。如果你机器上装过别的 JDK 或 JRE,这里经常会显示老版本,那就是Path里其他 Java 路径排在前面了,把老的去掉或把%JAVA_HOME%\bin移到最前就行。

2.2 IDEA 里创建 JavaFX 项目的两种方式

IDEA 社区版和 Ultimate 版都能做 JavaFX 开发,用社区版就行。新建项目时,IDEA 默认模板里其实没有 JavaFX 选项,所以有两种方式:

第一种,用 Maven 骨架。直接用命令行生成:

mvn archetype:generate -DarchetypeGroupId=org.openjfx -DarchetypeArtifactId=javafx-archetype-fxml -DarchetypeVersion=0.0.6 -DgroupId=com.example -DartifactId=renamer -Dversion=1.0.0

生成完用 IDEA 打开,它会自动识别为 Maven 项目并下载依赖。

第二种,直接创建一个普通 Java Maven 项目,然后自己在 pom.xml 里加入 JavaFX 依赖。这种方式更灵活,不用担心模板里的 FXML 文件要不要删。我下面的示例就按这个方式走,因为我觉得做内部工具时用纯 Java 代码写界面比 FXML 文件更直观,少一层文件跳转。

2.3 pom.xml 的核心配置

创建好 Maven 项目后,pom.xml 里加入 JavaFX 依赖。这里有一个容易被忽略的点:你只需要引入javafx-controls,它会自动带上javafx-graphics和javafx-base。

<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <javafx.version>17.0.2</javafx.version> </properties> <dependencies> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>${javafx.version}</version> </dependency> </dependencies>

如果后面你想用 FXML 分离界面,再补一个javafx-fxml依赖。我这次的项目不引入 FXML,所以控制界面、控制逻辑都集中在 Java 类里,对一个 500 行不到的小工具来说,反而更好维护。

2.4 第一次运行:No JavaFX runtime 怎么处理

写到这,很多入门的人会卡在第一步:写了个Application子类,直接在 IDEA 里点运行,结果报错:“JavaFX runtime components are missing, and are required to run this application”。

这个报错的本质是,JavaFX 要求自己必须作为模块加载,不能老实地躺在 classpath 里当普通 jar。IDEA 直接运行 JavaFX 应用时,需要让它把 JavaFX 作为模块暴露出去。在 IDEA 的 Run Configuration 里,找到 VM options 一栏,加上:

--module-path=target\classes --add-modules=javafx.controls

如果你用的是javafx-archetype-fxml模板,其实不用手动配,模板里已经带了javafx-maven-plugin,你直接运行mvn javafx:run就行。

不过我这里更推荐一种一劳永逸的配置:在 pom.xml 里加上javafx-maven-plugin,之后所有运行都用 Maven 来执行,不依赖 IDEA 的运行配置。

<build> <plugins> <plugin> <groupId>org.openjfx</groupId> <artifactId>javafx-maven-plugin</artifactId> <version>0.0.8</version> <configuration> <mainClass>com.example.renamer.Main</mainClass> </configuration> </plugin> </plugins> </build>

然后命令行执行:

mvn javafx:run

这是我在开发阶段最常用的方式,因为它的启动参数是 Maven 插件按 JavaFX 标准自动生成的,环境不会有偏差。

3. 应用开发实战:做一个批量重命名工具

3.1 工具需求与界面规划

我选了一个最能体现桌面应用实用性的场景:批量重命名文件。很多人电脑里有大量照片、下载文件、导出报表,文件名乱七八糟,手动改到崩溃。这种工具做起来逻辑不复杂,但界面非常直观,适合作为完整例子。

界面结构我用 JavaFX 的 BorderPane 布局:顶部放工具栏(ToolBar),中间是文件列表(TableView),底部放状态栏。工具栏从左到右依次是“选择文件夹”按钮、“前缀输入框”、“执行重命名”按钮。“选择文件夹”事件里用 DirectoryChooser 弹系统目录选择框,选中后递归读取目录下所有文件并展示在 TableView 中。

核心 Code 大致长这样:

public class Main extends Application { private final ObservableList<FileItem> fileItems = FXCollections.observableArrayList(); private TableView<FileItem> tableView; private TextField prefixField; private File currentDir; @Override public void start(Stage stage) { BorderPane root = new BorderPane(); ToolBar toolbar = new ToolBar(); Button chooseBtn = new Button("选择文件夹"); prefixField = new TextField("renamed_"); Button renameBtn = new Button("执行重命名"); toolbar.getItems().addAll(chooseBtn, new Label("前缀:"), prefixField, renameBtn); tableView = new TableView<>(); TableColumn<FileItem, String> nameCol = new TableColumn<>("当前文件名"); nameCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty()); nameCol.setPrefWidth(400); TableColumn<FileItem, String> newNameCol = new TableColumn<>("新文件名"); newNameCol.setCellValueFactory(cellData -> cellData.getValue().newNameProperty()); newNameCol.setPrefWidth(400); tableView.getColumns().add(nameCol); tableView.getColumns().add(newNameCol); tableView.setItems(fileItems); root.setTop(toolbar); root.setCenter(tableView); chooseBtn.setOnAction(e -> chooseDirectory(stage)); renameBtn.setOnAction(e -> doRename()); Scene scene = new Scene(root, 850, 550); stage.setTitle("批量重命名工具"); stage.setScene(scene); stage.show(); } private void chooseDirectory(Stage stage) { DirectoryChooser chooser = new DirectoryChooser(); File dir = chooser.showDialog(stage); if (dir != null) { currentDir = dir; loadFiles(dir); prefixField.requestFocus(); } } private void loadFiles(File dir) { fileItems.clear(); File[] files = dir.listFiles(); if (files == null) return; for (File file : files) { if (file.isFile()) { fileItems.add(new FileItem(file.getName(), "")); } } tableView.refresh(); } }

FileItem 是一个简单的记录类,用 JavaFX 的 SimpleStringProperty 包装文件名和新名称,这样表格列可以实时刷新。

3.2 重命名逻辑与防覆盖处理

重命名功能不复杂,但要注意两点:一是文件扩展名不能被拼音掉,二是新文件名不能和目录里已有文件冲突。我给重命名规则设计成“前缀 + 三位序号 + 原扩展名”,比如renamed_001.jpg、renamed_002.jpg。

private void doRename() { if (currentDir == null || fileItems.isEmpty()) { showAlert("请先选择文件夹"); return; } String prefix = prefixField.getText().trim(); if (prefix.isEmpty()) { showAlert("请输入前缀"); return; } Set<String> existingNames = new HashSet<>(); for (File f : currentDir.listFiles()) { existingNames.add(f.getName().toLowerCase()); } int index = 1; for (FileItem item : fileItems) { File original = new File(currentDir, item.getName()); String extension = ""; String name = item.getName(); int dotIndex = name.lastIndexOf('.'); if (dotIndex > 0) { extension = name.substring(dotIndex); } String newName = prefix + String.format("%03d", index) + extension; File target = new File(currentDir, newName); if (existingNames.contains(newName.toLowerCase())) { newName = prefix + String.format("%03d_", index) + System.currentTimeMillis() % 1000 + extension; target = new File(currentDir, newName); } if (original.renameTo(target)) { existingNames.add(newName.toLowerCase()); item.setNewName(newName); } index++; } loadFiles(currentDir); }

这段代码里,防冲突处理是实战里必踩的坑。如果你不做冲突检查,批量重命名到一半突然有一个文件重名,整个流程就中断了。处理逻辑也很直白:先收集当前目录所有文件名,重命名前检查目标名是否存在,如果存在就拼接一个时间戳后缀,保证一定能执行成功。

3.3 线程问题:别卡死界面

很多人第一次写 JavaFX 应用,喜欢在按钮事件里直接做耗时操作。如果只是遍历几百个文件问题不大,但你如果以后把工具扩展成处理大文件复制、压缩,就必须考虑线程问题。JavaFX 的界面线程叫 FX Application Thread,所有 UI 更新必须在这个线程上执行,但耗时任务不能堵在这个线程。

简单的方案是用Task配合Platform.runLater。比如在doRename里,如果文件量很大,可以先在后台线程里执行重命名逻辑,再通过 Platform.runLater 把结果塞回 TableView。我这个批量重命名工具因为普通场景就是几十个文件,直接同步做完全没问题,但我在代码里还是留了 Task 的注释,提醒自己以后扩展的时候别踩死界面的坑。

如果文件量大,推荐的方向是把doRename改成:

Task<Void> task = new Task<>() { @Override protected Void call() { // 重命名逻辑 return null; } }; task.setOnSucceeded(e -> loadFiles(currentDir)); new Thread(task).start();

这能保证界面始终可响应,不会出现“未响应”的白屏窗口。

3.4 和 IDEA 联调时的体验

开发过程中我基本都是直接mvn javafx:run启动。这里提一个 IDEA 的小技巧:配置好 Maven 后,可以在 IDEA 右侧 Maven 面板里找到javafx:run这个 goal,双击直接运行。第一次跑起来会下载一堆依赖,需要等一会儿,之后就很快了。

窗口起来以后,选一个测试目录,点“执行重命名”,看表格里的新文件名字段是否跟着刷新。我在开发时总会准备一个放满测试文件的文件夹,里面故意放几个名字相同的文件,用来验证防冲突逻辑。这一步跑通,后面打包成 exe 后就不用再频繁反复测试了。

4. 打包成 exe:jpackage 完整实战

4.1 jpackage 的前置条件

打包之前,先确认命令行能直接调用jpackage。JDK17 自带这个工具,路径在%JAVA_HOME%\bin\jpackage.exe。你可以运行:

jpackage --version

如果提示找不到命令,说明%JAVA_HOME%\bin不在 Path 里,或者你当前用的是 JRE 而不是 JDK。这里有个常见的误区:只安装了 JRE 的机器上是没有 jpackage 的,必须完整安装 JDK。

还有一个容易忽略的条件:jpackage 打包 exe 时,内部依赖了 WiX Toolset。JDK16 以后 jpackage 已经内置了轻量级的 WiX 组件,不需要你再手动装 WiX,这点比早期版本省事很多。如果你用更老的 JDK14/15 去打包,则需要额外下载 WiX 3.0 以上版本并配置环境变量,非常麻烦。所以用 JDK17 是绕开这个坑最直接的办法。

4.2 先构建普通 jar

jpackage 打包时需要一个可运行的 jar,作为应用的入口。先用 Maven 构建:

mvn clean package

构建完到target/目录下看,会有一个renamer-1.0.jar。但你直接双击这个 jar 是跑不起来的,因为它没有 Main-Class 属性,而且即使有,JavaFX 也不在 classpath 里。

为了让 jpackage 能正确找到入口,需要在 pom.xml 里配置 manifest。我用 maven-jar-plugin 指定 Main-Class:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <mainClass>com.example.renamer.Main</mainClass> </manifest> </archive> </configuration> </plugin>

同时还需要把 JavaFX 相关的依赖 jar 复制到一个统一目录,打包时让 jpackage 当作模块路径里的内容加载。最简单的方式是:

mvn dependency:copy-dependencies -DoutputDirectory=target/lib

这条命令会把 javafx-controls、javafx-graphics、javafx-base 等所有依赖 jar 复制到target/lib。注意 JavaFX 的 jar 包自带 module-info,所以它们可以出现在模块路径上,这是后面能打包成功的关键。

4.3 非模块化项目的 jpackage 命令

很多 JavaFX 教程要求你必须写module-info.java,走模块化路线。真实项目里,除非你从零开始完全控制依赖,否则第三方库动不动就破模块化规则,坑很多。所以我这次的批量重命名工具走的是“非模块化应用 + jpackage 模块路径”这条路,代码简单,打包也不复杂。

完整的打包命令如下(Windows cmd 环境):

set JAVA_HOME=D:\soft\jdk-17 set PATH=%JAVA_HOME%\bin;%PATH% mkdir dist jpackage --type exe ^ --name Renamer ^ --input target ^ --main-jar renamer-1.0.jar ^ --main-class com.example.renamer.Main ^ --module-path "%JAVA_HOME%\jmods;target\lib" ^ --add-modules javafx.controls ^ --icon src\main\resources\icon.ico ^ --dest dist ^ --app-version 1.0.0 ^ --vendor "Demo" ^ --win-menu ^ --win-shortcut ^ --win-dir-chooser

这里几个参数是重点:

  • --input target:表示把 target 目录下的所有内容作为应用输入,会自动带上renamer-1.0.jar和lib/目录。
  • --main-jar renamer-1.0.jar:指定入口 jar。
  • --module-path:这里有两段,第一段是 JDK 自带的 jmods,第二段是前面复制出来的 JavaFX 依赖 jar。jpackage 会根据这两个路径组装一个最小运行时。
  • --add-modules javafx.controls:告诉 jlink 把 JavaFX 的 controls 模块加进运行时,否则就算依赖放在模块路径里,也不会被链接进去。
  • --win-menu和--win-shortcut:让安装包自动创建开始菜单和桌面快捷方式。
  • --win-dir-chooser:安装过程中允许用户选择安装目录,这个对于分发给同事特别有用,不然默认只能装到 Program Files,权限问题很烦。

如果打包过程中提示 javafx 相关模块找不到,多半是target/lib目录里 javafx 的 jar 不在,重新执行一遍mvn dependency:copy-dependencies即可。

4.4 生成 app-image 和 exe 安装包

上面的命令会生成一个 exe 安装程序,目标文件在dist/Renamer-1.0.exe。双击它会走 Windows 标准的安装流程,安装完成后从快捷方式启动,程序就能正常运行。由于 jpackage 把精简的 JRE 和 JavaFX 运行时一起打包进安装目录,目标机器上不需要预装任何 JDK 或 JRE。

如果你不想走安装流程,比如自己拷给同事直接免安装运行,可以用--type app-image替代--type exe:

jpackage --type app-image ^ --name Renamer ^ --input target ^ --main-jar renamer-1.0.jar ^ --main-class com.example.renamer.Main ^ --module-path "%JAVA_HOME%\jmods;target\lib" ^ --add-modules javafx.controls ^ --dest dist

生成的是一个dist/Renamer/文件夹,里面有几个关键子目录:

  • Renamer.exe:真正的启动器
  • app/:存放应用 jar 和依赖 jar
  • runtime/:精简后的 JDK 运行时

你把整个 Renamer 文件夹拷到任意 Windows 机器上,双击里面的Renamer.exe就能运行。这在团队内部使用的时候非常方便,不用分发安装包,也不用担心卸载残留。

4.5 打包完成后验证与体积优化

我第一次打包完,exe 安装包大概 55MB,安装完占磁盘 130MB 左右。这个体积在现在动辄几百 MB 的 Electron 应用面前,已经算轻量了。如果还想更小,可以考虑在 jpackage 时用--strip-native-commands,或者手动精简 jmods 目录里的模块。但说实话,内部工具 50MB 完全可以接受,不用过度优化。

验证时我一般先在打包机器上跑一遍,再去一台“干净”的 Windows 虚拟机里装一遍测试。重点看两点:第一,是否提示缺 DLL 或 Java 环境;第二,双击快捷方式后 JavaFX 窗口能否正常弹出。只要这两点没问题,就可以放心发给别人了。

5. 常见报错与排查技巧实录

5.1 “JavaFX runtime components are missing” 的两种情况

这个报错是 JavaFX 新手最容易遇到的,背后含义是 JavaFX 没有以模块方式加载。在 IDEA 里运行报错,按 2.4 节说的,用javafx-maven-plugin的mvn javafx:run启动就行。但在打包后的 exe 里报同样错误,就需要查 jpackage 命令里的--add-modules参数,确认是否加了javafx.controls,同时确认target/lib里有没有 javafx 相关 jar。

我之前遇到过一种情况:用 jpackage 打包时忘了--module-path里带第二段target/lib,结果依赖 jar 虽然被复制出来了,jpackage 却根本没把它当模块路径看,运行时自然找不到 JavaFX。所以检查时优先看命令里的模块路径是不是两条都写了。

5.2 jpackage 提示 WiX 相关错误怎么办

前面说了 JDK16 以后 jpackage 内置了 WiX,但有些环境下还是会报找不到 WiX 工具的错误。常见原因是jpackage.exe所在 JDK 版本低于 16,或者JAVA_HOME指向了 JRE 而不是 JDK。你可以在命令行里执行:

%JAVA_HOME%\bin\jpackage --version

如果打印出来的版本是 15 或更低,直接换 JDK17。另外还有一种情况是系统里装了老版本 JDK,但命令行用的是新版本 JDK 的 jpackage,这个版本不一致的现象很隐蔽,建议在打包脚本开头显式指定set JAVA_HOME=D:\soft\jdk-17和set PATH=%JAVA_HOME%\bin;%PATH%。

5.3 打包后的 exe 双击没反应或闪退

这个问题通常在开发机器上不容易出现,因为它大概率跟目标机器缺少字体、显卡驱动或系统版本有关。如果你自己机器上双击闪退,先临时在 jpackage 命令里加一个--win-console参数,它会额外弹出一个命令行窗口,把 Java 的异常堆栈打出来。

jpackage --type exe --win-console ...

这种控制台模式非常有用,一旦崩了就能看到具体异常信息。实际中我遇到过的问题是 JavaFX 的硬件加速在部分老显卡上启动失败,解决方法是给启动器加系统属性,禁用硬件加速。在 jpackage 打包时可以通过--java-options传入 JVM 参数:

--java-options "-Dprism.order=sw"

这个参数强制 JavaFX 使用软件渲染,虽然性能稍有损失,但兼容性最好。如果你做的工具只是表格、表单这类 UI,完全感觉不到差别。

5.4 “cannot determine path to 'tools.jar' library for 17” 怎么处理

热词里有人搜过这个报错,我也踩过。这个错误一般不是 JavaFX 本身的问题,而是某些老构建插件、老版 Maven 插件在 JDK9 之后仍然尝试寻找tools.jar。JDK9 开始,JDK 目录里已经没有tools.jar了,相关 API 都移入了jdk.compiler模块。

解决办法是找到那个引用了 tools.jar 的配置。最常见的是 Maven 的maven-compiler-plugin版本过老,升级到 3.8.1 以上,或者检查 pom.xml 里有没有<compilerArguments>这种老式参数,把它去掉。IDEA 里如果还有这种问题,检查 Project Structure 里的 SDK 配置,确保选的是 JDK17 而不是一个被坑爬过的 JRE。

如果你看到这个报错时已经用了较新的 Maven 插件,还可以试试加一个依赖:

<dependency> <groupId>jdk.tools</groupId> <artifactId>jdk.tools</artifactId> <version>1.8</version> <scope>system</scope> <systemPath>${java.home}/../lib/tools.jar</systemPath> </dependency>

但注意,这只是临时绕过,不建议长期依赖,因为 tools.jar 在新 JDK 上不存在,万一别人换更新的 JDK,构建环境会更乱。

5.5 杀毒软件误报与图标问题

jpackage 生成的 exe 没有数字签名,所以 360、火绒、Windows Defender 有时候会弹风险提示。这个问题没有百分之百必然的解法,内部工具一般就是团队内约定加白名单,或者用企业证书对 exe 做签名。产品级使用者还是建议购买代码签名证书,否则用户信任成本很高。

另一个坑是图标格式。jpackage 的--icon参数要求.ico文件,不能拿一张 PNG 改名硬充。而且 ico 里最好包含 16、32、48、256 多个尺寸,否则部分 Windows 界面下快捷方式图标会很模糊。制作多尺寸 ico 最简单的方式是找一个在线转换工具,或者用 Python 的 Pillow 库生成多尺寸 ico 文件:

from PIL import Image img = Image.open("icon.png") img.save("icon.ico", sizes=[(16,16),(32,32),(48,48),(256,256)])

把这张 ico 放到src/main/resources/下,打包命令里用相对路径或绝对路径引用它就行。

最后分享一个小习惯

这套流程跑通之后,我把 jpackage 的完整命令整理成了一个build.bat,扔在项目根目录。脚本里用变量管理版本号、应用名、图标路径,每次更新版本只需要改一行变量。比如这次改了代码,执行build.bat,等几十秒,dist目录下就是一个全新的 exe。我把这个脚本也分享到这里,你根据自己的项目名调整即可:

@echo off setlocal set APP_NAME=Renamer set APP_VERSION=1.0.0 set MAIN_JAR=renamer-1.0.jar set MAIN_CLASS=com.example.renamer.Main set JAVA_HOME=D:\soft\jdk-17 set PATH=%JAVA_HOME%\bin;%PATH% call mvn clean package || goto :error call mvn dependency:copy-dependencies -DoutputDirectory=target/lib || goto :error mkdir dist 2>nul jpackage --type exe ^ --name %APP_NAME% ^ --input target ^ --main-jar %MAIN_JAR% ^ --main-class %MAIN_CLASS% ^ --module-path "%JAVA_HOME%\jmods;target\lib" ^ --add-modules javafx.controls ^ --icon src\main\resources\icon.ico ^ --dest dist ^ --app-version %APP_VERSION% ^ --win-menu ^ --win-shortcut ^ --win-dir-chooser || goto :error echo. echo 打包成功: dist\%APP_NAME%-%APP_VERSION%.exe exit /b 0 :error echo. echo 打包失败,请检查上一步输出。 exit /b 1

说句实在话,只要环境搭好、命令顺手,JavaFX 和 JDK17 这套组合做内部桌面工具非常靠谱。界面不丑、开发不慢、发布不折腾,遇到问题也能靠 Java 生态堆出解决方案。如果你正在几个技术栈之间犹豫,不妨拿这个批量重命名工具当练兵项目,从 0 到 exe 走一遍,感受会比看十篇文章都直观。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询