Java Swing背景
Java Swing是从1.6时代开始成熟的Java桌面应用GUI框架,也是以前大学里做算法毕设时走Java栈码农的必备开发能力之一。虽然现如今Java的桌面端能力已有多种更优秀的替代方案,例如JavaFX、Eclipse RCP等,但作为经典GUI框架,Swing以其简单的编码模式、优秀的跨平台能力、JRE默认自带包以及较小的JAR分发包,一直是我的Java桌面应用开发首选。
Java Swing缺点
Swing作为桌面应用有个较大的缺陷,其实也是Java在普通软件使用者里遇到的问题就是需要安装JRE环境,这个对于基于C++、C#开发的桌面应用相比,增加了用户的使用门槛,但如果分发软件时带上了JRE后又会增加了太多体积,更甚者,Swing的运行效率跟原生应用相比慢上了好几倍。上述的问题也是Java栈码农写桌面应用历来的痛点问题。
Java AOT技术简介
随着Java技术的发展,AOT技术如雨后春笋般涌现,从最早的Android ART替代Dalvik虚拟机,到GraalVM替代普通JDK,从原理上用一句话解释就是将动态语言的JIT过程通过AOT进行静态化。Java在Native这条道路上越走越宽,随之带来的好处就是开发者不需改变任何代码就可以让Java程序的运行性能媲美原生应用。
GraalVM特性简介
GraalVM的发布将JVM的适用范围从Java扩展到了Scala、Kotlin、Groovy、Clojure、R、Python、JavaScript和Ruby,从本质上讲GraalVM允许开发人员在单个应用程序中以多种语言和库高效地运行代码。其中最令我感到兴奋的就是Native Image组件,它是一种将Java代码提前编译为独立可执行文件的技术,该执行文件里包含了应用程序类、依赖、运行时库以及JDK静态连接的本机代码。相比基于运行在普通JVM的Java程序,其具备更快的启动时间和更低的运行时开销,并且可以完全脱离JVM环境。
本文目标
既然GraalVM Native Image这么强,那么Swing程序是否同样可以编译为独立可执行文件呢?我特意翻找了GraalVM的讨论帖,发现该问题早在2019年就有人提出过Issue(https://github.com/oracle/graal/issues/1327),但直到如今一直都没有Close,那么事实真的如此么?本着折腾心理,在空暇时我对Swing程序Native化做了不少的实验也踩了不少坑,终于在最近成功实现了目标。
因此本文的目的很简单,分享利用GraalVM的Native Image功能将Swing程序在Windows系统下Native化的Step by Step过程,最终生成脱离JVM的exe可执行程序。虽然本文是以Swing程序的Native化作为教程分享目标,但实际上也同样适用于所有Java程序的Native化,希望可以具备参考意义。
环境准备
系统
Windows 7+ (64位)
GraalVM环境
GraalVM Community Edition 21.2.0 (Java8)
将GraalVM加入到系统环境变量中
安装组件:
Native Image Installable SVM 21.2.0
安装组件命令:
gu install -L native-image-installable-svm-java8-windows-amd64-21.2.0.jar
下载地址:
https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-21.2.0
注意点:
Java8的GraalVM最高仅支持到了21.3.1,但该版本存在一些bug,因此只能选择次新的21.2.0版本。
Visual Studio环境
Visual Studio Community 2017+
安装组件:
Visual C++
Swing程序
由于编译Native Image的特殊性,我们需要在Swing程序的main方法入口最开始处强制设置一个环境变量,否则在完成后面的编译后,运行会失败:
System.setProperty("java.home", ".");
将Swing程序编译打包为jar文件,并且将依赖库打入Jar中,取名:test.jar。正常运行Swing程序的命令为:
java -jar test.jar
开始编译Native Image
进入VC编译环境
开始菜单找到并打开 “Visual Studio 2017\Visual Studio Tools\VC\适用于 VS 2017 的 x64 本机工具命令提示”
进入Jar文件目录
假设Jar文件目录名为native-test,目录里只有test.jar文件,我们通过cd命令进入到native-test目录内。
以代理类模式运行Jar文件采集meta信息
此步骤的意义在于让Native Image在Swing应用运行过程中监控到所有运行时动态加载的类,包括jni加载类、代理类、反射类、静态资源文件等,这些类必须要应用正常运行时才能感知到,无法通过系统简单静态引用分析获取。通过以下命令可以将采集到的所有meta信息保存在native-image子目录下:
java -agentlib:native-image-agent=config-output-dir=native-image -jar test.jar
采集的步骤可以通过以下方式重复运行,以达到人为干预触达到应用所有分支逻辑的作用,config-merge-dir命令可以将所有采集到的meta信息进行去重合并:
java -agentlib:native-image-agent=config-merge-dir=native-image -jar test.jar
以下是采集完成后生成的meta信息文件:
手工补充采集到的meta文件
此步骤主要是因为Native Image对于Swing应用的运行时类监控存在缺陷,没有将必要的系统类加入到meta信息中,需要手工补充进去,否则在完成编译后运行文件时会报错找不到类。
native-image\jni-config.json 添加以下内容到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>"
}
]
}
native-image\jni-config.json 找到java.awt.image.IndexColorModel、sun.java2d.InvalidPipeException json节点,里面添加以下内容:
"methods": [
{
"name": "<init>"
}
]
native-image\reflect-config.json 添加以下内容到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>"
}
]
}
构建文件
通过以下命令进行正式native编译,编译过程会比较久:
native-image -jar test.jar -H:+ReportExceptionStackTraces --no-fallback --allow-incomplete-classpath -H:ConfigurationFileDirectories=native-image -H:+AddAllCharsets --report-unsupported-elements-at-runtime --enable-url-protocols=https,http
解释下每个参数的含义:
以下是构建完成后生成的结果,test.exe文件在70M左右:
补充缺失的文件
此时直接执行test.exe文件还是会报错,这是因为Native Image在引入依赖文件时漏下了2个配置文件,我们到GraalVM的安装目录内找到 jre\lib下的flavormap.properties和fontconfig.bfc文件,复制到test.exe所在目录的lib子目录下:
最终在去除掉所有的中间文件后,文件列表如下所示,共计73M左右:
此时我们双击运行test.exe文件就会得到与执行java -jar test.jar 一样的运行效果了。
简单测试
我们同时启动Jar和Naive化后的exe文件对比后发现,内存的使用明显减少,启动速度更快,并且不依赖JVM环境。因此使用GraalVM Native Image将Java Swing应用Native化的目标已经达成。