使用GraalVM Native Image将Java Swing应用Native化

简介: Java Swing背景        Java Swing是从1.6时代开始成熟的Java桌面应用GUI框架,也是以前大学里做算法毕设时走Java栈码农的必备开发能力之一。虽然现如今Java的桌面端能力已有多种更优秀的替代方案,例如JavaFX、Eclipse RCP等,但作为经典GUI框架,Swing以其简单的编码模式、优秀的跨平台能力、JRE默认自带包以及较小的JAR分发包,一直是我的Jav

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

        解释下每个参数的含义:

-H:+ReportExceptionStackTraces

显示构建期间的异常堆栈跟踪

--no-fallback

构建不依赖JVM的native image或显示构建失败

--allow-incomplete-classpath

允许使用不完整的类路径构建,该参数适用于依赖了弱引用的库

-H:ConfigurationFileDirectories=native-image

配置采集到的meta信息的配置文件目录地址

-H:+AddAllCharsets

支持所有字符集,不加该参数会导致中文乱码

--report-unsupported-elements-at-runtime

在运行native image时才报错不受支持的方法和字段,而不是在构建期间报错

--enable-url-protocols=https,http

支持的URL协议,不加该参数会导致无法访问web地址

        以下是构建完成后生成的结果,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化的目标已经达成。

目录
相关文章
|
22天前
|
移动开发 Java Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【4月更文挑战第3天】在移动开发领域,性能优化一直是开发者关注的焦点。随着Kotlin的兴起,其在Android开发中的地位逐渐上升,但关于其与Java在性能方面的对比,尚无明确共识。本文通过深入分析并结合实际测试数据,探讨了Kotlin与Java在Android平台上的性能表现,揭示了在不同场景下两者的差异及其对应用性能的潜在影响,为开发者在选择编程语言时提供参考依据。
|
24天前
|
缓存 算法 Java
Java内存管理与调优:释放应用潜能的关键
【4月更文挑战第2天】Java内存管理关乎性能与稳定性。理解JVM内存结构,如堆和栈,是优化基础。内存泄漏是常见问题,需谨慎管理对象生命周期,并使用工具如VisualVM检测。有效字符串处理、选择合适数据结构和算法能提升效率。垃圾回收自动回收内存,但策略调整影响性能,如选择不同类型的垃圾回收器。其他优化包括调整堆大小、使用对象池和缓存。掌握这些技巧,开发者能优化应用,提升系统性能。
|
25天前
|
存储 Java 关系型数据库
个人成绩信息管理系统【GUI/Swing+MySQL】(Java课设)
个人成绩信息管理系统【GUI/Swing+MySQL】(Java课设)
20 0
|
25天前
|
存储 Java 关系型数据库
社区医院管理服务系统【GUI/Swing+MySQL】(Java课设)
社区医院管理服务系统【GUI/Swing+MySQL】(Java课设)
25 1
|
25天前
|
存储 Java 关系型数据库
实验室设备管理系统【GUI/Swing+MySQL】(Java课设)
实验室设备管理系统【GUI/Swing+MySQL】(Java课设)
17 0
|
25天前
|
存储 Java 关系型数据库
冬奥会传统文化管理系统【GUI/Swing+MySQL】(Java课设)
冬奥会传统文化管理系统【GUI/Swing+MySQL】(Java课设)
8 0
|
22天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【4月更文挑战第3天】 在Java并发编程中,线程池是一种重要的资源管理工具,它能有效地控制和管理线程的数量,提高系统性能。本文将深入探讨Java线程池的工作原理、应用场景以及优化策略,帮助读者更好地理解和应用线程池。
|
3天前
|
安全 Java 调度
Java线程:深入理解与实战应用
Java线程:深入理解与实战应用
20 0
|
1天前
|
安全 Java 大数据
探索Java的奇妙世界:语言特性与实际应用
探索Java的奇妙世界:语言特性与实际应用
|
3天前
|
Java
Java中的并发编程:理解和应用线程池
【4月更文挑战第23天】在现代的Java应用程序中,性能和资源的有效利用已经成为了一个重要的考量因素。并发编程是提高应用程序性能的关键手段之一,而线程池则是实现高效并发的重要工具。本文将深入探讨Java中的线程池,包括其基本原理、优势、以及如何在实际开发中有效地使用线程池。我们将通过实例和代码片段,帮助读者理解线程池的概念,并学习如何在Java应用中合理地使用线程池。