使用 IDEA / jlink / jpackage 打包 Java 项目至 .exe 三两事

更上层楼,更上层楼——

前一段时间粗略学习了 Maven 构建工具的使用,并且成功写了一个小项目。当你看到这篇文章的时候我已经把 ScreenShoot 托管到了 Github 平台,源码在彼处,此处大致介绍一下 releases 下 ScreenShoot-1.0-portable 的构建流程。

对于水平尚缺的我刚开始钻研时简直一波三折,心路历程如下:

首先一开始是确定了思路:.jar + jdk + .bat ,其中 .bat 仅仅作为启动器,里面内容约为

jdk-21.0.9\bin\java -jar screenshoot.jar

云云。(除此之外我还学习到了 cd %~dp0 命令,用以切换到脚本所在文件夹,再也不用手动写死目录呜呜)

先讲讲 .jar 的由来。按理来说我正在学习 Maven ,理应使用相关插件在 IDEA 点击 Maven -> Lifecycle -> package 之类的操作。可也正好由于初学不精,思想着第一次做项目 pom.xml 应轻装简从(点击这里看看!),就搁置了,反而采用另一个 IDEA 自带的方法。

没错,就是构建工件。 Files -> Project Structure -> artifacts(工件) -> 【添加 JAR】 -> From Modules with Dependencies… -> 【添加主类】。
确定之后就可以在 Build -> Build Artifacts 里成功打包 .jar 文件了。默认在项目根目录下的 out\artifacts 文件夹。

至此我们获得了项目打包的 .jar 文件。应该可以使用 java -jar 命令运行了。

分发的第二个问题显而易见,在于 jdk 巨大的存储空间占用,我打包完成后的 .jar 文件不到 1 MB ,可整个 jdk-21.0.9 文件夹如下:

是的,太臃肿。四方打听之后我找到了 jlink 的方案。

jlink 是什么?我理解为自定义 jre ,可以根据需求定制自己的伪 jdk ,只提取出自己真正所需的模块。

首先运行

jdeps -s app.jar

命令找到 .jar 文件真正所需的模块,输出如下:

app.jar -> java.base
app.jar -> java.datatransfer
app.jar -> java.desktop
app.jar -> java.logging

jdeps <Options> <Path> 命令用来分析类的依赖关系,参数 -s(ummary) 简化了输出内容,感兴趣的可以去掉之后试试。

至此我找到了四个模块,再运行一下 jlink 命令:

jlink ^
	--module-path %JAVA_HOME%\jmods ^
	--add-modules java.base,java.datatransfer,java.desktop,java.logging ^
	--compress=2 ^
	--no-header-files ^
	--strip-debug ^
	--output .\runtime

逐行解释一下:
--module-path %JAVA_HOME%\jmods 参数,表示在这个文件夹寻找所需要的模块;
--add-modules java.base,java.datatransfer,java.desktop,java.logging 参数,表示提取这四个模块;
--compress=2 参数,表示压缩级别,可选 0、1、2。追求轻量的我自然填入 2 (但提示已过时??QAQ);
接下来两个参数 --no-header-files--strip-debug 分别表示删除不必要的头文件、删除调试信息,过程不太好懂,但最终结果都还是减少了文件存储大小。
最后的参数 --output .\runtime 指定提取出的文件夹。最终结果如下,还是勉强能够接受了:

最后是最难的做启动器部分了。我一开始凭借浅薄的经验真的在 run.bat 写了

java -jar app.jar

这种写法运行的 Java 程序与命令行窗口绑定,并且关闭窗口就关闭程序。

又查询到了 javaw ,这是专门用以后台运行 Java 程序的工具。说来惭愧,好多年前一穷二白时就在捣鼓 Minecraft 的 Java 版本,纠结于到底选择 java.exe 还是 javaw.exe 以及各自区别。现在的我已经了然,而不似小学初中稀里糊涂之间买的桂花与酒与江上之载舟。

现在换成

javaw -jar app.jar

这一下 Java 程序确实跑到后台了,但 CMD 的窗口没有自然消失,下一行添加 exit 后也不消失,看来 CMD 认为现在仍然在执行这条程序?(实测好像 PowerShell 不这样QTQ)

查询之后找到新方法:改用 start 启动空窗口:

start "" javaw -jar app.jar

成功,黑窗口一闪而过,用户也知道了程序正常启动。

然后我开始纠结于程序的自定义 .ico 图标,并且考虑到完全的小白似乎有可能认不到 run.bat ,所以我还是想要去做成 .exe 。于是我找到了 https://github.com/l-urk/Bat-To-Exe-Converter-64-Bit 。

最终因为频繁报毒中道崩殂。

当天我睡前突发奇想改写成 .ps1 文件,在 Windows 10 自带的低版本 PowerShell 运行 ps2exe (高版本的 PowerShell反而运行不了)。我以前已经学习过,但还是此处贴出教程。 首先运行

Install-Module ps2exe -Scope CurrentUser

意思是给当前用户安装模块,去掉 -Scope CurrentUser 后会因为权限问题失败,所以去不掉。 然后可以直接运行

Invoke-ps2exe <inputPath> <OutputPath> -iconFile <iconPath>

来打包了。

按理说此时大功告成,但是打开任务管理器,找到我的 ScreenShoot,却赫然显示:

心想着还是尽我所能做到完美吧。于是再次辗转找到了 jpackage 工具。这也是 JDK 自带的工具包,且专门用来打包 .exe 程序。

在项目文件夹准备如下结构:

ScreenShoot/
├── input/
│   └── app.jar
├── runtime/
│   └── ...
└── icon.ico

然后直接运行命令

jpackage ^
	--name ScreenShoot ^
	--input input ^
	--main-jar app.jar ^
	--runtime-image runtime ^
	--icon icon.ico ^
	--type exe ^
	--dest output

再次逐行分析:
--name 参数,表示程序名称,诸如在任务管理器中不显示 Java(TM) Platform SE binary
--input 参数,后面填写文件夹路径,表示在此处寻找并打包程序核心文件;
--main-jar app.jar 参数,在 --input 参数的文件夹下确定程序入口主类;
--runtime-image runtime 参数,用以自定义 runtime 文件夹;其实如果不写也会自动调用 jlink 打包,但是为了全面与保险会添加更多的模块,以使得不如之前手动操作来得轻量;
--icon icon.ico 参数,填写图标的路径。
注意。我自己学到这里时也大开眼界。.ico 文件并不是自己使用 PhotoShop 裁剪工具做到 256×256 像素,再一行 ffmpeg -i icon.png icon.ico 就了事的。真正的 .ico 文件内含 16×1632×32256×256 不同的尺寸,以适应不同大小的窗口的显示。这一步我在 https://redketchup.io/icon-editor 完成。(ScreenShoot 的图标来自浮光光光光处的约稿,含义或许是 あや 拿着相机拍摄?闭嘴!)
--type app-image 参数,表示 jpackage 打包的 .exe 程序类型,若填写 exe 或者 msi 就是安装程序了,不行不行(?)。
--dest 表示最终的输出文件夹,可以前去一览丰姿了。至此大功告成。

这样做的另一个好处是,程序运行的部分依赖文件,例如 JNativeHook.x86_64.dll,也生成了到了与 app.jar 同级的 app 目录。但我在程序内部写的 screenshots 文件夹和 app.log 文件没有跟着一起转移,仍然在根目录。我在写下这句话时尚未明白原因,日后再去查一下 File 类以及相关的 Maven 项目结构吧。

这是 alice.fan 博客的第一篇文章。很多人都说自建博客的第一篇文章和最后一篇文章都是“从零开始搭建自己的博客”。我还是不以为然,也因为 LNMP 结构对每一个拥有服务器的人我觉得确实应该了如指掌,所以不对 WordPress 多废笔墨了。即便这途中我也遇到重重困苦,但每一个新名词都构成我的崭新认知不是吗。伪静态、https 协议、nginx 反向代理。现在我的网站都已跨越。

你我的人生也是不断地跨越。

← 返回首页