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

目录
相关文章
|
11天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
20天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
13天前
|
SQL Java 数据库连接
从理论到实践:Hibernate与JPA在Java项目中的实际应用
本文介绍了Java持久层框架Hibernate和JPA的基本概念及其在具体项目中的应用。通过一个在线书店系统的实例,展示了如何使用@Entity注解定义实体类、通过Spring Data JPA定义仓库接口、在服务层调用方法进行数据库操作,以及使用JPQL编写自定义查询和管理事务。这些技术不仅简化了数据库操作,还显著提升了开发效率。
28 3
|
23天前
|
SQL 监控 Java
技术前沿:Java连接池技术的最新发展与应用
本文探讨了Java连接池技术的最新发展与应用,包括高性能与低延迟、智能化管理和监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,为开发者提供了一份详尽的技术指南。
30 7
|
21天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
35 3
|
21天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
40 2
|
22天前
|
缓存 Java 数据库连接
Hibernate:Java持久层框架的高效应用
通过上述步骤,可以在Java项目中高效应用Hibernate框架,实现对关系数据库的透明持久化管理。Hibernate提供的强大功能和灵活配置,使得开发者能够专注于业务逻辑的实现,而不必过多关注底层数据库操作。
12 1
|
26天前
|
移动开发 前端开发 JavaScript
java家政系统成品源码的关键特点和技术应用
家政系统成品源码是已开发完成的家政服务管理软件,支持用户注册、登录、管理个人资料,家政人员信息管理,服务项目分类,订单与预约管理,支付集成,评价与反馈,地图定位等功能。适用于各种规模的家政服务公司,采用uniapp、SpringBoot、MySQL等技术栈,确保高效管理和优质用户体验。
|
26天前
|
SQL 监控 Java
Java性能优化:提升应用效率与响应速度的全面指南
【10月更文挑战第21】Java性能优化:提升应用效率与响应速度的全面指南
|
23天前
|
Java 开发者
Java中的多线程基础与应用
【10月更文挑战第24天】在Java的世界中,多线程是提高效率和实现并发处理的关键。本文将深入浅出地介绍如何在Java中创建和管理多线程,以及如何通过同步机制确保数据的安全性。我们将一起探索线程生命周期的奥秘,并通过实例学习如何优化多线程的性能。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开一扇通往高效编程的大门。
17 0
下一篇
无影云桌面