使用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化的目标已经达成。

目录
相关文章
|
2月前
|
人工智能 安全 Java
Java和Python在企业中的应用情况
Java和Python在企业中的应用情况
69 7
|
2月前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
181 3
|
15天前
|
安全 算法 Java
Java CAS原理和应用场景大揭秘:你掌握了吗?
CAS(Compare and Swap)是一种乐观锁机制,通过硬件指令实现原子操作,确保多线程环境下对共享变量的安全访问。它避免了传统互斥锁的性能开销和线程阻塞问题。CAS操作包含三个步骤:获取期望值、比较当前值与期望值是否相等、若相等则更新为新值。CAS广泛应用于高并发场景,如数据库事务、分布式锁、无锁数据结构等,但需注意ABA问题。Java中常用`java.util.concurrent.atomic`包下的类支持CAS操作。
46 2
|
15天前
|
存储 IDE Java
漂亮不是梦!Java Swing美化攻略
Java Swing 是一个为 Java 设计的 GUI 工具包,提供文本框、按钮等组件。尽管其外观可定制,通过 Look and Feel(LAF)机制改变应用风格,如 Darcula 和 FlatLaf,但现已淡出主流视野,主要应用于 IDE 领域,如 IntelliJ IDEA 和 Eclipse。相比其他 GUI 框架,Swing 的发展前景有限。
38 1
|
2月前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
177 6
|
1月前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
40 2
|
2月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
70 6
|
2月前
|
关系型数据库 MySQL Java
MySQL索引优化与Java应用实践
【11月更文挑战第25天】在大数据量和高并发的业务场景下,MySQL数据库的索引优化是提升查询性能的关键。本文将深入探讨MySQL索引的多种类型、优化策略及其在Java应用中的实践,通过历史背景、业务场景、底层原理的介绍,并结合Java示例代码,帮助Java架构师更好地理解并应用这些技术。
60 2
|
2月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
2月前
|
Java 测试技术 API
Java 反射机制:深入解析与应用实践
《Java反射机制:深入解析与应用实践》全面解析Java反射API,探讨其内部运作原理、应用场景及最佳实践,帮助开发者掌握利用反射增强程序灵活性与可扩展性的技巧。
129 4