Java Swing 桌面程序使用 GraalVM 封装为 exe 文件进行Native化

背景

本文主要基于如下两点情况,进行的实际案例,并记录的操作步骤。

  • 使用 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.propertiesfontconfig.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.IndexColorModelsun.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 PackerEnigma Virtual Box 等),将 dll、exe 及相关文件封装成一个单一的 exe 文件,最终效果就是只有一个 exe 文件了,这样传播和发送会更方便。

再或者你也可以使用 Inno Setup 等第三方工具将这个文件夹中的文件封装成一个安装文件包,他人双击你的安装文件会有一个安装过程,最终生产快捷方式使用。这种也是最不会出问题的,因为它底层最终就是一个解压缩还原过程。

选择适合自己的方式处理即可,这些第三方工具的使用细节,本文不做赘述。


(END)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/581693.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringMVC整体工作流程

. 用户发起一个请求&#xff0c;请求首先到达前端控制器前端控制器接收到请求后会调用处理器映射器&#xff0c;由此得知&#xff0c;这个请求该由哪一个Controller来进行处理(并未调用Controller)&#xff1b;前端控制器调用处理器适配器&#xff0c;告诉处理器适配器应该要…

甘特图是什么?利用甘特图来优化项目管理流程

在现代项目管理中,图表是一种强大而直观的工具,可以帮助项目经理和团队成员清晰地了解并掌控整个项目进程。其中,甘特图是最常用和最有效的图表之一。 甘特图是一种条形图,可以用来直观地展示项目中各个任务的进度、持续时间和相互关系。它由一个横轴和一个纵轴组成。横轴代表时…

[LitCTF 2023]Ping、[SWPUCTF 2021 新生赛]error、[NSSCTF 2022 Spring Recruit]babyphp

[LitCTF 2023]Ping 尝试ping一下127.0.0.1成功了&#xff0c;但要查看根目录时提示只能输入IP 查看源代码&#xff0c;这段JavaScript代码定义了一个名为check_ip的函数&#xff0c;用于验证输入是否为有效的IPv4地址。并且使用正则表达式re来匹配IPv4地址的格式。 对于这种写…

【计算机组成原理 1】计算机硬件概念

0️⃣ 参考 王道计算机考研408 1️⃣ 冯诺依曼机 核心思想【存储程序】 存储程序就是将指令先放入内存中&#xff0c;再从内存读取指令执行&#xff0c;从而实现自动化。核心 【运算器】 说明&#xff1a;在计算机系统中&#xff0c;软件和硬件在逻辑上是等效的 例如&#xf…

Debian 系统设置SSH 连接时长

问题现象&#xff1a; 通过finalshell工具连接Debian系统远程操作时&#xff0c;总是一下断开一下断开&#xff0c;要反复重新连接 &#xff0c;烦人&#xff01; 解决办法&#xff1a; 找到ssh安装目录下的配置文件&#xff1a;sshd_config vi sshd_config &#xff1a; 找到…

基于Springboot+Vue的Java项目-火车票订票系统开发实战(附演示视频+源码+LW)

大家好&#xff01;我是程序员一帆&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &am…

Matlab实现CNN-LSTM模型,对一维时序信号进行分类

1、利用Matlab2021b训练CNN-LSTM模型&#xff0c;对采集的一维时序信号进行分类二分类或多分类 2、CNN-LSTM时序信号多分类执行结果截图 训练进度&#xff1a; 网络分析&#xff1a; 指标变化趋势&#xff1a; 代码下载方式&#xff08;代码含数据集与模型构建&#xff0c;附…

基于Springboot的爱心商城系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的爱心商城系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&…

Idea异常 | Process 453 is still running

问题现象 Idea启动报错"Cannot connect to already running IDE instance. Exception: Process 453 is still running" 问题原因 通常原因是Idea未正常关闭&#xff0c;导致进程锁文件没有删除。同样Pycharm等其它JeBrains等产品也有可能出现这个问题 解决办法 查…

图像预处理工具_CogImageFileTool

CogImageFileTool工具可以用来将单张图片或idb格式的图片数据库读入内存。也可使用CoglmageFileTool工具将图片插入到.idb数据库里。 添加工具 参数介绍 文件名 写入模式 读取模式 删除

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-6.4

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

Django初步了解

目录 一、什么是Django 二、Django的设计模式 三、涉及的英文缩写及其含义 四、安装&#xff08;官方教程&#xff09; 一、什么是Django Django是一个Python Web框架&#xff0c;可以快速开发网站&#xff0c;提供一站式的解决方案&#xff0c;包括缓存、数据库ORM、后台…

CAPS Wizard for Mac:打字输入辅助应用

CAPS Wizard for Mac是一款专为Mac用户设计的打字输入辅助应用&#xff0c;以其简洁、高效的功能&#xff0c;为用户带来了全新的打字体验。 CAPS Wizard for Mac v5.3激活版下载 该软件能够智能预测用户的输入内容&#xff0c;实现快速切换和自动大写锁定&#xff0c;从而大大…

用Jenkins实现cherry-pick多个未入库的gerrit编译Android固件

背景: 在做Android固件开发的时候,通常我们可以利用gerrit-trigger插件,开发者提交一笔的时候自动触发jenkins编译,如果提交的这一笔的编译依赖其他gerrit才能编译过,我们可以在commit message中加入特殊字段,让jenkins在编译此笔patch的时候同时抓取依赖的gerrit代码下…

Maven介绍 主要包括Maven的基本介绍,作用,以及对应的Maven模型,可以对Maven有一个基本的了解

1、Maven介绍 1.1 什么是Maven Maven是Apache旗下的一个开源项目&#xff0c;是一款用于管理和构建java项目的工具。 官网&#xff1a;https://maven.apache.org/ Apache 软件基金会&#xff0c;成立于1999年7月&#xff0c;是目前世界上最大的最受欢迎的开源软件基金会&…

【办公类-22-13】周计划系列(5-5)“周计划-05 周计划表格内教案部分“节日”清空改成“节日“” (2024年调整版本)Win32

背景需求&#xff1a; 本学期19周&#xff0c;用了近10周的时间&#xff0c;终于把周计划教案部分的内容补全了&#xff08;把所有教案、反思的文字都撑满一个单元格&#xff09;&#xff0c; 一、原始教案 二、新模板内的教案 三、手动添加文字后的样式&#xff08;修改教案…

第 4 篇 : Netty客户端互发图片和音/视频

说明 因为图片和音/视频不能确定其具体大小, 故引入MinIO。客户端之间只发送消息, 通过上传/下载来获取额外信息 1. MinIO搭建(参考前面文章), 并启动 2. 登录MinIO创建3个Bucket: image、voice、video 3. 客户端改造 3.1 修改 pom.xml <?xml version"1.0" …

党建3d互动虚拟现实网上展厅有何优势?

在数字化浪潮席卷全球的今天&#xff0c;企业如何迅速踏上虚拟世界的征程&#xff0c;开启元宇宙之旅?答案就是——3D虚拟云展。这一创新平台&#xff0c;华锐视点以虚拟现实技术和3D数字建模为基石提供3D云展搭建服务&#xff0c;助力企业轻松搭建起虚拟数字基础设施&#xf…

【linux】动静态库的使用与制作

本章节是基础IO的的最后一个话题!! 目录 浅谈一下动静态库&#xff1a;动静态库的制作与使用&#xff1a;静态库&#xff1a;怎么办&#xff1a;方法一&#xff1a;方法二&#xff1a;方法三&#xff1a;方法四&#xff1a; 是什么&#xff1a;为什么&#xff1a; 动态库&#…

机器学习:基于Sklearn、XGBoost框架,使用逻辑回归、支持向量机和XGBClassifier预测帕金森病

前言 系列专栏&#xff1a;机器学习&#xff1a;高级应用与实践【项目实战100】【2024】✨︎ 在本专栏中不仅包含一些适合初学者的最新机器学习项目&#xff0c;每个项目都处理一组不同的问题&#xff0c;包括监督和无监督学习、分类、回归和聚类&#xff0c;而且涉及创建深度学…