<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>七色寮</title>
<link>https://blog.alice.fan</link>
<description>你再也不会遇见第二个我。</description>
<item>
<title>使用 IDEA / jlink / jpackage 打包 Java 项目至 .exe 三两事</title>
<link>https://blog.alice.fan/posts/package-my-first-java-project.html</link>
<guid isPermaLink="true">https://blog.alice.fan/posts/package-my-first-java-project.html</guid>
<pubDate>Mon, 20 Apr 2026 00:00:00 +0800</pubDate>
<description><![CDATA[<p>前一段时间粗略学习了 Maven 构建工具的使用，并且成功写了一个小项目。当你看到这篇文章的时候我已经把 <code>ScreenShoot</code> 托管到了 Github 平台，源码在<a href="https://github.com/rainbowist77/screenshoot" target="_blank" rel="noopener">彼处</a>，此处大致介绍一下 releases 下 <code>ScreenShoot-1.0-portable</code> 的构建流程。</p>
<p>对于水平尚缺的我刚开始钻研时简直一波三折，心路历程如下：</p>
<p>首先一开始是确定了思路：<code>.jar</code> + jdk + <code>.bat</code> ，其中 <code>.bat</code> 仅仅作为启动器，里面内容约为</p>
<pre><code class="language-cmd">jdk-<span class="tok-num">21.0</span>.<span class="tok-num">9</span>\bin\java -jar screenshoot.jar</code></pre>
<p>云云。（除此之外我还学习到了 <code>cd %~dp0</code> 命令，用以切换到脚本所在文件夹，再也不用手动写死目录呜呜）</p>
<p>先讲讲 <code>.jar</code> 的由来。按理来说我正在学习 Maven ，理应使用相关插件在 IDEA 点击 <code>Maven</code> -&gt; <code>Lifecycle</code> -&gt; <code>package</code> 之类的操作。可也正好由于初学不精，思想着第一次做项目 <code>pom.xml</code> 应轻装简从（点击<a href="https://github.com/rainbowist77/screenshoot/blob/main/pom.xml" target="_blank" rel="noopener">这里</a>看看！），就搁置了，反而采用另一个 IDEA 自带的方法。</p>
<p>没错，就是构建工件。 <code>Files</code> -&gt; <code>Project Structure</code> -&gt; <code>artifacts（工件）</code> -&gt; 【添加 JAR】 -&gt; <code>From Modules with Dependencies…</code> -&gt; 【添加主类】。<br>确定之后就可以在 <code>Build -&gt; Build Artifacts</code> 里成功打包 <code>.jar</code> 文件了。默认在项目根目录下的 <code>out\artifacts</code> 文件夹。</p>
<p>至此我们获得了项目打包的 <code>.jar</code> 文件。应该可以使用 <code>java -jar</code> 命令运行了。</p>
<p>分发的第二个问题显而易见，在于 jdk 巨大的存储空间占用，我打包完成后的 <code>.jar</code> 文件不到 1 MB ，可整个 <code>jdk-21.0.9</code> 文件夹如下：<br><img src="https://blog.alice.fan/assets/image/1-1.png" alt="" loading="lazy"><br>是的，太臃肿。四方打听之后我找到了 jlink 的方案。</p>
<p>jlink 是什么？我理解为自定义 jre ，可以根据需求定制自己的伪 jdk ，只提取出自己真正所需的模块。</p>
<p>首先运行</p>
<pre><code class="language-cmd">jdeps -s app.jar</code></pre>
<p>命令找到 <code>.jar</code> 文件真正所需的模块，输出如下：</p>
<pre><code class="language-cmd">app.jar -&gt; java.base
app.jar -&gt; java.datatransfer
app.jar -&gt; java.desktop
app.jar -&gt; java.logging</code></pre>
<p><code>jdeps &lt;Options&gt; &lt;Path&gt;</code> 命令用来分析类的依赖关系，参数 <code>-s(ummary)</code> 简化了输出内容，感兴趣的可以去掉之后试试。</p>
<p>至此我找到了四个模块，再运行一下 <code>jlink</code> 命令：</p>
<pre><code class="language-cmd">jlink ^
	--module-path <span class="tok-var">%JAVA_HOME%</span>\jmods ^
	--add-modules java.base,java.datatransfer,java.desktop,java.logging ^
	--compress=<span class="tok-num">2</span> ^
	--no-header-files ^
	--strip-debug ^
	--output .\runtime</code></pre>
<p>逐行解释一下：<br><code>--module-path %JAVA_HOME%\jmods</code> 参数，表示在这个文件夹寻找所需要的模块；<br><code>--add-modules java.base,java.datatransfer,java.desktop,java.logging</code> 参数，表示提取这四个模块；<br><code>--compress=2</code> 参数，表示压缩级别，可选 0、1、2。追求轻量的我自然填入 2 （但提示已过时？？QAQ）；<br>接下来两个参数 <code>--no-header-files</code>、<code>--strip-debug</code> 分别表示删除不必要的头文件、删除调试信息，过程不太好懂，但最终结果都还是减少了文件存储大小。<br>最后的参数 <code>--output .\runtime</code> 指定提取出的文件夹。最终结果如下，还是勉强能够接受了：<br><img src="https://blog.alice.fan/assets/image/1-2.png" alt="" loading="lazy"></p>
<p>最后是最难的做启动器部分了。我一开始凭借浅薄的经验真的在 <code>run.bat</code> 写了</p>
<pre><code class="language-cmd">java -jar app.jar</code></pre>
<p>这种写法运行的 Java 程序与命令行窗口绑定，并且关闭窗口就关闭程序。</p>
<p>又查询到了 <code>javaw</code> ，这是专门用以后台运行 Java 程序的工具。说来惭愧，好多年前一穷二白时就在捣鼓 Minecraft 的 Java 版本，纠结于到底选择 <code>java.exe</code> 还是 <code>javaw.exe</code> 以及各自区别。现在的我已经了然，而不似小学初中稀里糊涂之间买的桂花与酒与江上之载舟。</p>
<p>现在换成</p>
<pre><code class="language-cmd">javaw -jar app.jar</code></pre>
<p>这一下 Java 程序确实跑到后台了，但 CMD 的窗口没有自然消失，下一行添加 <code>exit</code> 后也不消失，看来 CMD 认为现在仍然在执行这条程序？（实测好像 PowerShell 不这样QTQ）</p>
<p>查询之后找到新方法：改用 <code>start</code> 启动空窗口：</p>
<pre><code class="language-cmd"><span class="tok-kw">start</span> <span class="tok-str">""</span> javaw -jar app.jar</code></pre>
<p>成功，黑窗口一闪而过，用户也知道了程序正常启动。</p>
<p>然后我开始纠结于程序的自定义 <code>.ico</code> 图标，并且考虑到完全的小白似乎有可能认不到 <code>run.bat</code> ，所以我还是想要去做成 <code>.exe</code> 。于是我找到了 https://github.com/l-urk/Bat-To-Exe-Converter-64-Bit 。</p>
<p>最终因为频繁报毒中道崩殂。</p>
<p>当天我睡前突发奇想改写成 <code>.ps1</code> 文件，在 Windows 10 自带的低版本 <code>PowerShell</code> 运行 <code>ps2exe</code> （高版本的 PowerShell反而运行不了）。我以前已经学习过，但还是此处贴出教程。 首先运行</p>
<pre><code class="language-powershell">Install-Module ps2exe -Scope CurrentUser</code></pre>
<p>意思是给当前用户安装模块，去掉 <code>-Scope CurrentUser</code> 后会因为权限问题失败，所以去不掉。 然后可以直接运行</p>
<pre><code class="language-powershell">Invoke-ps2exe &lt;inputPath&gt; &lt;OutputPath&gt; -iconFile &lt;iconPath&gt;</code></pre>
<p>来打包了。</p>
<p>按理说此时大功告成，但是打开任务管理器，找到我的 <code>ScreenShoot</code>，却赫然显示：<br><img src="https://blog.alice.fan/assets/image/1-3.png" alt="" loading="lazy"><br>心想着还是尽我所能做到完美吧。于是再次辗转找到了 <code>jpackage</code> 工具。这也是 JDK 自带的工具包，且专门用来打包 <code>.exe</code> 程序。</p>
<p>在项目文件夹准备如下结构：</p>
<pre><code>ScreenShoot/
├── input/
│   └── app.jar
├── runtime/
│   └── ...
└── icon.ico</code></pre>
<p>然后直接运行命令</p>
<pre><code class="language-cmd">jpackage ^
	--name ScreenShoot ^
	--input input ^
	--main-jar app.jar ^
	--runtime-image runtime ^
	--icon icon.ico ^
	--type exe ^
	--dest output</code></pre>
<p>再次逐行分析：<br><code>--name</code> 参数，表示程序名称，诸如在任务管理器中不显示 <code>Java(TM) Platform SE binary</code>；<br><code>--input</code> 参数，后面填写文件夹路径，表示在此处寻找并打包程序核心文件；<br><code>--main-jar app.jar</code> 参数，在 <code>--input</code> 参数的文件夹下确定程序入口主类；<br><code>--runtime-image runtime</code> 参数，用以自定义 runtime 文件夹；其实如果不写也会自动调用 <code>jlink</code> 打包，但是为了全面与保险会添加更多的模块，以使得不如之前手动操作来得轻量；<br><code>--icon icon.ico</code> 参数，填写图标的路径。<br>注意。我自己学到这里时也大开眼界。<code>.ico</code> 文件并不是自己使用 PhotoShop 裁剪工具做到 <code>256×256</code> 像素，再一行 <code>ffmpeg -i icon.png icon.ico</code> 就了事的。真正的 <code>.ico</code> 文件内含 <code>16×16</code>、<code>32×32</code> 到 <code>256×256</code> 不同的尺寸，以适应不同大小的窗口的显示。这一步我在 https://redketchup.io/icon-editor 完成。（<code>ScreenShoot</code> 的图标来自<a href="https://www.mihuashi.com/profiles/346521" target="_blank" rel="noopener">浮光光光光</a>处的约稿，含义或许是 あや 拿着相机拍摄？闭嘴！）<br><code>--type app-image</code> 参数，表示 <code>jpackage</code> 打包的 <code>.exe</code> 程序类型，若填写 <code>exe</code> 或者 <code>msi</code> 就是安装程序了，不行不行（？）。<br><code>--dest</code> 表示最终的输出文件夹，可以前去一览丰姿了。至此大功告成。</p>
<p>这样做的另一个好处是，程序运行的部分依赖文件，例如 <code>JNativeHook.x86_64.dll</code>，也生成了到了与 <code>app.jar</code> 同级的 <code>app</code> 目录。但我在程序内部写的 <code>screenshots</code> 文件夹和 <code>app.log</code> 文件没有跟着一起转移，仍然在根目录。我在写下这句话时尚未明白原因，日后再去查一下 <code>File</code> 类以及相关的 Maven 项目结构吧。</p>
<p>这是 <a href="https://alice.fan" target="_blank" rel="noopener">alice.fan</a> 博客的第一篇文章。很多人都说自建博客的第一篇文章和最后一篇文章都是“从零开始搭建自己的博客”。我还是不以为然，也因为 <code>LNMP</code> 结构对每一个拥有服务器的人我觉得确实应该了如指掌，所以不对 WordPress 多废笔墨了。即便这途中我也遇到重重困苦，但每一个新名词都构成我的崭新认知不是吗。伪静态、<code>https</code> 协议、<code>nginx</code> 反向代理。现在我的网站都已跨越。</p>
<p>你我的人生也是不断地跨越。</p>]]></description>
<content:encoded><![CDATA[<p>前一段时间粗略学习了 Maven 构建工具的使用，并且成功写了一个小项目。当你看到这篇文章的时候我已经把 <code>ScreenShoot</code> 托管到了 Github 平台，源码在<a href="https://github.com/rainbowist77/screenshoot" target="_blank" rel="noopener">彼处</a>，此处大致介绍一下 releases 下 <code>ScreenShoot-1.0-portable</code> 的构建流程。</p>
<p>对于水平尚缺的我刚开始钻研时简直一波三折，心路历程如下：</p>
<p>首先一开始是确定了思路：<code>.jar</code> + jdk + <code>.bat</code> ，其中 <code>.bat</code> 仅仅作为启动器，里面内容约为</p>
<pre><code class="language-cmd">jdk-<span class="tok-num">21.0</span>.<span class="tok-num">9</span>\bin\java -jar screenshoot.jar</code></pre>
<p>云云。（除此之外我还学习到了 <code>cd %~dp0</code> 命令，用以切换到脚本所在文件夹，再也不用手动写死目录呜呜）</p>
<p>先讲讲 <code>.jar</code> 的由来。按理来说我正在学习 Maven ，理应使用相关插件在 IDEA 点击 <code>Maven</code> -&gt; <code>Lifecycle</code> -&gt; <code>package</code> 之类的操作。可也正好由于初学不精，思想着第一次做项目 <code>pom.xml</code> 应轻装简从（点击<a href="https://github.com/rainbowist77/screenshoot/blob/main/pom.xml" target="_blank" rel="noopener">这里</a>看看！），就搁置了，反而采用另一个 IDEA 自带的方法。</p>
<p>没错，就是构建工件。 <code>Files</code> -&gt; <code>Project Structure</code> -&gt; <code>artifacts（工件）</code> -&gt; 【添加 JAR】 -&gt; <code>From Modules with Dependencies…</code> -&gt; 【添加主类】。<br>确定之后就可以在 <code>Build -&gt; Build Artifacts</code> 里成功打包 <code>.jar</code> 文件了。默认在项目根目录下的 <code>out\artifacts</code> 文件夹。</p>
<p>至此我们获得了项目打包的 <code>.jar</code> 文件。应该可以使用 <code>java -jar</code> 命令运行了。</p>
<p>分发的第二个问题显而易见，在于 jdk 巨大的存储空间占用，我打包完成后的 <code>.jar</code> 文件不到 1 MB ，可整个 <code>jdk-21.0.9</code> 文件夹如下：<br><img src="https://blog.alice.fan/assets/image/1-1.png" alt="" loading="lazy"><br>是的，太臃肿。四方打听之后我找到了 jlink 的方案。</p>
<p>jlink 是什么？我理解为自定义 jre ，可以根据需求定制自己的伪 jdk ，只提取出自己真正所需的模块。</p>
<p>首先运行</p>
<pre><code class="language-cmd">jdeps -s app.jar</code></pre>
<p>命令找到 <code>.jar</code> 文件真正所需的模块，输出如下：</p>
<pre><code class="language-cmd">app.jar -&gt; java.base
app.jar -&gt; java.datatransfer
app.jar -&gt; java.desktop
app.jar -&gt; java.logging</code></pre>
<p><code>jdeps &lt;Options&gt; &lt;Path&gt;</code> 命令用来分析类的依赖关系，参数 <code>-s(ummary)</code> 简化了输出内容，感兴趣的可以去掉之后试试。</p>
<p>至此我找到了四个模块，再运行一下 <code>jlink</code> 命令：</p>
<pre><code class="language-cmd">jlink ^
	--module-path <span class="tok-var">%JAVA_HOME%</span>\jmods ^
	--add-modules java.base,java.datatransfer,java.desktop,java.logging ^
	--compress=<span class="tok-num">2</span> ^
	--no-header-files ^
	--strip-debug ^
	--output .\runtime</code></pre>
<p>逐行解释一下：<br><code>--module-path %JAVA_HOME%\jmods</code> 参数，表示在这个文件夹寻找所需要的模块；<br><code>--add-modules java.base,java.datatransfer,java.desktop,java.logging</code> 参数，表示提取这四个模块；<br><code>--compress=2</code> 参数，表示压缩级别，可选 0、1、2。追求轻量的我自然填入 2 （但提示已过时？？QAQ）；<br>接下来两个参数 <code>--no-header-files</code>、<code>--strip-debug</code> 分别表示删除不必要的头文件、删除调试信息，过程不太好懂，但最终结果都还是减少了文件存储大小。<br>最后的参数 <code>--output .\runtime</code> 指定提取出的文件夹。最终结果如下，还是勉强能够接受了：<br><img src="https://blog.alice.fan/assets/image/1-2.png" alt="" loading="lazy"></p>
<p>最后是最难的做启动器部分了。我一开始凭借浅薄的经验真的在 <code>run.bat</code> 写了</p>
<pre><code class="language-cmd">java -jar app.jar</code></pre>
<p>这种写法运行的 Java 程序与命令行窗口绑定，并且关闭窗口就关闭程序。</p>
<p>又查询到了 <code>javaw</code> ，这是专门用以后台运行 Java 程序的工具。说来惭愧，好多年前一穷二白时就在捣鼓 Minecraft 的 Java 版本，纠结于到底选择 <code>java.exe</code> 还是 <code>javaw.exe</code> 以及各自区别。现在的我已经了然，而不似小学初中稀里糊涂之间买的桂花与酒与江上之载舟。</p>
<p>现在换成</p>
<pre><code class="language-cmd">javaw -jar app.jar</code></pre>
<p>这一下 Java 程序确实跑到后台了，但 CMD 的窗口没有自然消失，下一行添加 <code>exit</code> 后也不消失，看来 CMD 认为现在仍然在执行这条程序？（实测好像 PowerShell 不这样QTQ）</p>
<p>查询之后找到新方法：改用 <code>start</code> 启动空窗口：</p>
<pre><code class="language-cmd"><span class="tok-kw">start</span> <span class="tok-str">""</span> javaw -jar app.jar</code></pre>
<p>成功，黑窗口一闪而过，用户也知道了程序正常启动。</p>
<p>然后我开始纠结于程序的自定义 <code>.ico</code> 图标，并且考虑到完全的小白似乎有可能认不到 <code>run.bat</code> ，所以我还是想要去做成 <code>.exe</code> 。于是我找到了 https://github.com/l-urk/Bat-To-Exe-Converter-64-Bit 。</p>
<p>最终因为频繁报毒中道崩殂。</p>
<p>当天我睡前突发奇想改写成 <code>.ps1</code> 文件，在 Windows 10 自带的低版本 <code>PowerShell</code> 运行 <code>ps2exe</code> （高版本的 PowerShell反而运行不了）。我以前已经学习过，但还是此处贴出教程。 首先运行</p>
<pre><code class="language-powershell">Install-Module ps2exe -Scope CurrentUser</code></pre>
<p>意思是给当前用户安装模块，去掉 <code>-Scope CurrentUser</code> 后会因为权限问题失败，所以去不掉。 然后可以直接运行</p>
<pre><code class="language-powershell">Invoke-ps2exe &lt;inputPath&gt; &lt;OutputPath&gt; -iconFile &lt;iconPath&gt;</code></pre>
<p>来打包了。</p>
<p>按理说此时大功告成，但是打开任务管理器，找到我的 <code>ScreenShoot</code>，却赫然显示：<br><img src="https://blog.alice.fan/assets/image/1-3.png" alt="" loading="lazy"><br>心想着还是尽我所能做到完美吧。于是再次辗转找到了 <code>jpackage</code> 工具。这也是 JDK 自带的工具包，且专门用来打包 <code>.exe</code> 程序。</p>
<p>在项目文件夹准备如下结构：</p>
<pre><code>ScreenShoot/
├── input/
│   └── app.jar
├── runtime/
│   └── ...
└── icon.ico</code></pre>
<p>然后直接运行命令</p>
<pre><code class="language-cmd">jpackage ^
	--name ScreenShoot ^
	--input input ^
	--main-jar app.jar ^
	--runtime-image runtime ^
	--icon icon.ico ^
	--type exe ^
	--dest output</code></pre>
<p>再次逐行分析：<br><code>--name</code> 参数，表示程序名称，诸如在任务管理器中不显示 <code>Java(TM) Platform SE binary</code>；<br><code>--input</code> 参数，后面填写文件夹路径，表示在此处寻找并打包程序核心文件；<br><code>--main-jar app.jar</code> 参数，在 <code>--input</code> 参数的文件夹下确定程序入口主类；<br><code>--runtime-image runtime</code> 参数，用以自定义 runtime 文件夹；其实如果不写也会自动调用 <code>jlink</code> 打包，但是为了全面与保险会添加更多的模块，以使得不如之前手动操作来得轻量；<br><code>--icon icon.ico</code> 参数，填写图标的路径。<br>注意。我自己学到这里时也大开眼界。<code>.ico</code> 文件并不是自己使用 PhotoShop 裁剪工具做到 <code>256×256</code> 像素，再一行 <code>ffmpeg -i icon.png icon.ico</code> 就了事的。真正的 <code>.ico</code> 文件内含 <code>16×16</code>、<code>32×32</code> 到 <code>256×256</code> 不同的尺寸，以适应不同大小的窗口的显示。这一步我在 https://redketchup.io/icon-editor 完成。（<code>ScreenShoot</code> 的图标来自<a href="https://www.mihuashi.com/profiles/346521" target="_blank" rel="noopener">浮光光光光</a>处的约稿，含义或许是 あや 拿着相机拍摄？闭嘴！）<br><code>--type app-image</code> 参数，表示 <code>jpackage</code> 打包的 <code>.exe</code> 程序类型，若填写 <code>exe</code> 或者 <code>msi</code> 就是安装程序了，不行不行（？）。<br><code>--dest</code> 表示最终的输出文件夹，可以前去一览丰姿了。至此大功告成。</p>
<p>这样做的另一个好处是，程序运行的部分依赖文件，例如 <code>JNativeHook.x86_64.dll</code>，也生成了到了与 <code>app.jar</code> 同级的 <code>app</code> 目录。但我在程序内部写的 <code>screenshots</code> 文件夹和 <code>app.log</code> 文件没有跟着一起转移，仍然在根目录。我在写下这句话时尚未明白原因，日后再去查一下 <code>File</code> 类以及相关的 Maven 项目结构吧。</p>
<p>这是 <a href="https://alice.fan" target="_blank" rel="noopener">alice.fan</a> 博客的第一篇文章。很多人都说自建博客的第一篇文章和最后一篇文章都是“从零开始搭建自己的博客”。我还是不以为然，也因为 <code>LNMP</code> 结构对每一个拥有服务器的人我觉得确实应该了如指掌，所以不对 WordPress 多废笔墨了。即便这途中我也遇到重重困苦，但每一个新名词都构成我的崭新认知不是吗。伪静态、<code>https</code> 协议、<code>nginx</code> 反向代理。现在我的网站都已跨越。</p>
<p>你我的人生也是不断地跨越。</p>]]></content:encoded>
</item>
</channel></rss>
