背景
本文主要基于如下两点情况,进行的实际案例,并记录的操作步骤。
-
使用 Java Swing 开发的小型桌面程序,运行需要依赖当前电脑安装 jre 环境,对使用者很不友好,且相比原生的 exe 程序偏慢。
-
GraalVM Native 允许开发人员使用 Native Image 组件将Java代码提前编译为独立可执行文件(windows 和 linux 都支持),封装后的执行文件里包含了应用程序类、依赖、运行时库以及JDK静态连接的本机代码。相比基于运行在普通JVM的Java程序,其具备更快的启动时间和更低的运行时开销,并且可以完全脱离JVM环境。
所以我们使用 GraalVM Native 来解决第一个描述的 Swing 缺点问题,封装后的不再依赖电脑安装 jdk,且能提高运行效率。
本文目标
将一个依赖了第三方 jar 包的 swing 桌面程序 jar 包,通过 graalvm native-image 打包为可以脱离 jvm 独立运行的 exe 文件。
代码工程说明
我的这个 Swing 程序,是一个小工具,依赖了两个第三方 jar 包,一个是日期选择控件,另一个是界面风格的库。
pom.xml 中添加的依赖及使用的用于将依赖的第三方jar包最终打包到一起的plugin插件配置如下:
<dependencies>
<dependency>
<groupId>com.toedter</groupId>
<artifactId>jcalendar</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId>
<version>3.4.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<!-- 保持Manifest的Main-Class属性 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.shanhy.plugin.license.LicenseGeneratorGUI</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
程序运行效果如下:
因为我们需要将这个 swing 程序编译为 exe,有一个特殊的地方需要处理,就是需要在程序的 main 方法入口,添加一行代码设置 java.home,如下:
/**
* 入口
*
* @param args args
*/
public static void main(String[] args) {
// 由于编译Native Image的特殊性,我们需要在Swing程序的main方法入口最开始处强制设置一个环境变量,否则在完成后面的编译后,运行会失败
System.setProperty("java.home", ".");
FlatLightLaf.setup();
SwingUtilities.invokeLater(LicenseGeneratorGUI::new);
}
使用 mvn clean package
打包后的工程结构如下图:
通过 cmd 命令行进入 target 目录,确保 java -jar quickcode-genlicense-1.0.0.jar
可以正常运行程序,但是如果运行出现了异常 java.lang.NullPointerException: Cannot load from short array because "sun.awt.FontConfiguration.head" is null
,则需要在 quickcode-genlicense-1.0.0.jar 的旁边创建一个 lib
目录,然后拷贝 jre/lib
中的 flavormap.properties
和 fontconfig.bfc
文件到 lib 目录中。
两个文件可以使用 Everything 等磁盘搜索工具快速搜索拷贝,如下图所示:
确保程序可以正常运行后,表示我们的 swing 程序是没有问题的,至此我们还没有涉及到跟 graalvm native 打包有关的内容。
开始编译Native Image
1、进入命令行环境
使用 java -version
确认当前 Java 环境变量已经配置 Graalvm
2、采集meta信息
进入jar目录,以代理类模式运行jar文件采集meta信息。
此步骤的意义在于让Native Image在Swing应用运行过程中监控到所有运行时动态加载的类,包括jni加载类、代理类、反射类、静态资源文件等,这些类必须要应用正常运行时才能感知到,无法通过系统简单静态引用分析获取。
通过以下命令可以将采集到的所有meta信息保存在 native-image 子目录下:
cd E:\code\workspace-shanhy\quickcode-genlicense\target
java -agentlib:native-image-agent=config-output-dir=E:/native-image -jar quickcode-genlicense-1.0.0.jar
成功打开程序 UI 界面后,需要把所有输入框、按钮等全部功能操作一遍, 特别是包含输入框中的复制、粘贴、剪切、输入法回车填充等等。
尽可能的把所有能操作的点全部操作一遍,因为你每遗漏的任何一个操作点都有可能导致你最后程序在进行相同功能操作时出现错误。
如果一次采集有遗漏,还可以重复运行命令,使用 java -agentlib:native-image-agent=config-merge-dir=E:/native-image -jar quickcode-genlicense-1.0.0.jar
补充收集。config-merge-dir
可以将所有采集到的 mata 信息进行去重合并。
在多人分工测试的情况时,我还是建议自己写一段代码程序,对多个人分别获得的 mate 元数据 json 文件进行去重合并,以形成最终的内容。
各种功能操作完成后,退出程序磁盘会生成采集的信息文件,以下是采集完成后生成的 meta 信息文件:
3、补充信息文件内容
此步骤主要是因为Native Image对于Swing应用的运行时类监控存在缺陷,没有将必要的系统类加入到meta信息中,需要手工补充进去,否则在完成编译后运行文件时会报错找不到类。
a) 添加以下内容到 jni-config.json
末尾节点
{
"name": "java.lang.Object",
"methods": [
{
"name": "toString"
}
]
},
{
"name": "java.lang.Class",
"methods": [
{
"name": "getComponentType"
}
]
},
{
"name": "java.lang.String",
"methods": [
{
"name": "getBytes"
},
{
"name": "toCharArray"
},
{
"name": "<init>"
}
]
},
{
"name": "java.lang.reflect.Method",
"methods": [
{
"name": "getParameterTypes"
},
{
"name": "getReturnType"
}
]
},
{
"name": "java.nio.Buffer",
"methods": [
{
"name": "position"
}
]
},
{
"name": "java.nio.ByteBuffer",
"methods": [
{
"name": "array"
},
{
"name": "arrayOffset"
}
]
},
{
"name": "java.nio.CharBuffer",
"methods": [
{
"name": "array"
},
{
"name": "arrayOffset"
}
]
},
{
"name": "java.nio.ShortBuffer",
"methods": [
{
"name": "array"
},
{
"name": "arrayOffset"
}
]
},
{
"name": "java.nio.IntBuffer",
"methods": [
{
"name": "array"
},
{
"name": "arrayOffset"
}
]
},
{
"name": "java.nio.LongBuffer",
"methods": [
{
"name": "array"
},
{
"name": "arrayOffset"
}
]
},
{
"name": "java.nio.FloatBuffer",
"methods": [
{
"name": "array"
},
{
"name": "arrayOffset"
}
]
},
{
"name": "java.nio.DoubleBuffer",
"methods": [
{
"name": "array"
},
{
"name": "arrayOffset"
}
]
},
{
"name": "java.lang.Void",
"fields": [
{
"name": "TYPE"
}
]
},
{
"name": "java.lang.Boolean",
"methods": [
{
"name": "<init>"
}
],
"fields": [
{
"name": "value"
},
{
"name": "TYPE"
}
]
},
{
"name": "java.lang.Byte",
"methods": [
{
"name": "<init>"
}
],
"fields": [
{
"name": "value"
},
{
"name": "TYPE"
}
]
},
{
"name": "java.lang.Character",
"methods": [
{
"name": "<init>"
}
],
"fields": [
{
"name": "value"
},
{
"name": "TYPE"
}
]
},
{
"name": "java.lang.Short",
"methods": [
{
"name": "<init>"
}
],
"fields": [
{
"name": "value"
},
{
"name": "TYPE"
}
]
},
{
"name": "java.lang.Integer",
"methods": [
{
"name": "<init>"
}
],
"fields": [
{
"name": "value"
},
{
"name": "TYPE"
}
]
},
{
"name": "java.lang.Long",
"methods": [
{
"name": "<init>"
}
],
"fields": [
{
"name": "value"
},
{
"name": "TYPE"
}
]
},
{
"name": "java.lang.Float",
"methods": [
{
"name": "<init>"
}
],
"fields": [
{
"name": "value"
},
{
"name": "TYPE"
}
]
},
{
"name": "java.lang.Double",
"methods": [
{
"name": "<init>"
}
],
"fields": [
{
"name": "value"
},
{
"name": "TYPE"
}
]
},
{
"name": "sun.java2d.d3d.D3DRenderQueue$1",
"methods": [
{
"name": "run"
}
]
},
{
"name": "sun.java2d.d3d.D3DGraphicsDevice$1",
"methods": [
{
"name": "run"
}
]
},
{
"name": "sun.java2d.d3d.D3DSurfaceData$1",
"methods": [
{
"name": "run"
}
]
},
{
"name": "sun.java2d.d3d.D3DSurfaceData",
"fields": [
{
"name": "nativeHeight"
},
{
"name": "nativeWidth"
}
]
},
{
"name": "java.awt.image.ComponentSampleModel",
"fields": [
{
"name": "pixelStride"
},
{
"name": "scanlineStride"
},
{
"name": "bandOffsets"
},
{
"name": "bankIndices"
},
{
"name": "numBands"
},
{
"name": "numBanks"
}
]
},
{
"name": "sun.awt.image.ByteComponentRaster",
"fields": [
{
"name": "bandOffset"
},
{
"name": "dataOffsets"
},
{
"name": "scanlineStride"
},
{
"name": "pixelStride"
},
{
"name": "data"
},
{
"name": "type"
}
]
},
{
"name": "java.awt.event.MouseWheelEvent",
"methods": [
{
"name": "<init>"
}
]
}
b) 在 jni-config.json
找到 java.awt.image.IndexColorModel
和 sun.java2d.InvalidPipeException
,在这2个节点中添加内容 "methods":[{"name": "<init>"}]
,示例如下
...
{
"name":"java.awt.image.IndexColorModel",
"methods":[{"name": "<init>"}],
"fields":[{"name":"allgrayopaque"}, {"name":"colorData"}, {"name":"lookupcache"}, {"name":"map_size"}, {"name":"rgb"}, {"name":"transparent_index"}]
}
...
{
"name":"sun.java2d.InvalidPipeException",
"methods":[{"name": "<init>"}]
}
...
c) 添加以下内容到 reflect-config.json
末尾节点
{
"name": "com.github.markusbernhardt.proxy.jna.win.WinHttp",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "sun.java2d.loops.SetDrawLineANY",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "sun.java2d.loops.SetFillRectANY",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "sun.java2d.loops.SetDrawRectANY",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "sun.java2d.loops.SetDrawPolygonsANY",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "sun.java2d.loops.SetDrawPathANY",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "sun.java2d.loops.SetFillPathANY",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "sun.java2d.loops.SetFillSpansANY",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "com.sun.mail.handlers.multipart_mixed",
"methods": [
{
"name": "<init>"
}
]
}
4、执行编译
在 Native Tool Command
,命令窗口中通过以下命令进行正式 native 编译,编译过程会比较久(电脑的CPU和内存性能越高速度可能会越快)。
native-image -jar quickcode-genlicense-1.0.0.jar -H:+ReportExceptionStackTraces --no-fallback --link-at-build-time -H:ConfigurationFileDirectories=E:/native-image -H:+AddAllCharsets --report-unsupported-elements-at-runtime --enable-url-protocols=https,http
解释下每个参数的含义:
-H:+ReportExceptionStackTraces | 显示构建期间的异常堆栈跟踪 |
---|---|
--no-fallback | 构建不依赖JVM的native image或显示构建失败 |
--link-at-build-time | 在构建镜像的时候报告类或包的链接错误 |
-H:ConfigurationFileDirectories=E:/native-image | 配置采集到的meta信息的配置文件目录地址 |
-H:+AddAllCharsets | 支持所有字符集,不加该参数会导致中文乱码 |
--report-unsupported-elements-at-runtime | 在运行native image时才报错不受支持的方法和字段,而不是在构建期间报错 |
--enable-url-protocols=https,http | 支持的URL协议,不加该参数会导致无法访问web地址 |
更多选项参数详见官网:https://www.graalvm.org/latest/reference-manual/native-image/overview/Options/
构建完成后,如果是类似我这种代码量和依赖比较少的简单桌面程序,生成的 exe 文件大约70MB左右,生成的 exe 和相关的 dll 文件如下图所示:
注:lib 中的 flavormap.properties 和 fontconfig.bfc 我们在前面已经拷贝过来了。
最后双击 exe 文件即可打开程序了,你会发现速度比 jar 包运行的 swing 程序速度快多了。
如果需要拷贝给其他人使用,需要将如图中红框圈起来的所有文件一起拷贝到一个文件夹中,不能只发送一个 exe 文件。
除去cmd弹框
运行 exe 文件后,默认会有一个 cmd 命令窗口,如下图所示:
我们只需要打开一个普通的 cmd 窗口,使用命令 editbin /subsystem:windows quickcode-genlicense-1.0.0.exe
对这个 exe 文件进行一个修改处理,就可以消除这个运行时的黑色命令窗口了,如下图所示:
文件夹封装
如果觉得每次分享给朋友使用都需要发送一个文件夹里面包含这些文件不是足够好,你还可以使用第三方工具(例如 Boxed App Packer
、Enigma Virtual Box
等),将 dll、exe 及相关文件封装成一个单一的 exe 文件,最终效果就是只有一个 exe 文件了,这样传播和发送会更方便。
再或者你也可以使用 Inno Setup
等第三方工具将这个文件夹中的文件封装成一个安装文件包,他人双击你的安装文件会有一个安装过程,最终生产快捷方式使用。这种也是最不会出问题的,因为它底层最终就是一个解压缩还原过程。
选择适合自己的方式处理即可,这些第三方工具的使用细节,本文不做赘述。
(END)