在maven环境中使用GraalVM来构建本地原生应用程序(一)构建本地可执行文件

本文主要是介绍在maven环境中使用GraalVM来构建本地原生应用程序(一)构建本地可执行文件,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言
  • 一、GraalVM安装
  • 二、初步使用
  • 三、踩坑记录
    • 1、JSON转换问题
    • 2、反射、资源、jni的调用问题
    • 3、HTTPS调用问题
    • 4、Linux下CPU架构问题
    • 5、Linux下GLIBC版本的问题
    • 6、部分Windows系统无法缺少相关的库文件
  • 总结


前言

随着Java17的更新,jdk又推出了一个GraalVM平台,关于GraalVM的相关资料大家可以去官网了解,点击这里进入官网。
什么是GraalVM?我感觉用一句话来解释就是:把Java程序编译成本机的可执行的二进制代码。之前的Java一直运行在JVM平台上,所谓的Java跨平台性,其实完全依赖的是JVM的跨平台性,我们发布的所有Java程序,都必须安装一个JVM的平台,这样在操作性上还是有很多不便。
其次最近几年流行的云原生应用多半会是未来微服务的趋势,Java作为微服务重要的成员,原生应用貌似迫在眉睫。
GraalVM目前还没有JVM成熟,各大Java生态也在推行,springboot3.0和quarkus也都在积极支持,说明GraalVM或许是Java开发的另外一条路子。
正好目前我再开发一个项目,这个项目对性能的要求很高,于是尝试了用GraalVM来构建,经过测试完全能满足目前的需求,但在使用过程中还是有很多不方便的地方,而且GraalVM对编码的要求很高,下面我给大家分享在使用过程中踩到的一些坑,我的开发环境是springboot生态,关于quarkus生态大家可以自行去研究。


一、GraalVM安装

首先进入官网进行下载,选择jdk版本和平台,我这里使用JDK17,如下图:
在这里插入图片描述
下载完后解压,会得到一个文件夹如:graalvm-jdk-17.0.9+11.1,进入到文件夹:
在这里插入图片描述
我这里结构如下,Home里面其实就是jdk环境,这是我们要修改JAVA_HOME环境变量。将JAVA_HOME的路径改到我们下载的这里,然后查看Java环境:

java -version

如下:

java version "17.0.9" 2023-10-17 LTS
Java(TM) SE Runtime Environment Oracle GraalVM 17.0.9+11.1 (build 17.0.9+11-LTS-jvmci-23.0-b21)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 17.0.9+11.1 (build 17.0.9+11-LTS-jvmci-23.0-b21, mixed mode, sharing)

如果有带GraalVM的信息,说明安装成功,另外我们也可以直接运行native-image:

native-image

输出:

Please specify options for native-image building or use --help for more info.

说明已经安装成功

二、初步使用

下面我们创建一个springboot的项目,这里springboot我们选择3.2.2,pom.xml文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.2</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.example</groupId><artifactId>test-sb-native</artifactId><version>0.0.1-SNAPSHOT</version><name>test-sb-native</name><description>test-sb-native</description><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>

这里最重要的是加入了native-maven-plugin这个插件,然后我们写点简单的代码方便我们测试:

package org.example.testsbnative;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@SpringBootApplication
@RestController
public class TestSbNativeApplication {public static void main(String[] args) {SpringApplication.run(TestSbNativeApplication.class, args);}@RequestMapping("/test")public Object test(){return "hello native";}
}

然后我们执行打包命令,这里的打包命令需要这样写:

mvn clean -DskipTests native:compile -Pnative

过程比较漫长,与电脑的性能有关系,等待打包结束,我们看到有如下信息输出表示成功:

------------------------------------------------------------------------------------------------------------------------
Top 10 origins of code area:                                Top 10 object types in image heap:16.18MB java.base                                            9.83MB byte[] for code metadata5.16MB tomcat-embed-core-10.1.18.jar                        3.83MB byte[] for java.lang.String4.66MB svm.jar (Native Image)                               2.95MB java.lang.Class3.90MB java.xml                                             2.92MB java.lang.String2.42MB jackson-databind-2.15.3.jar                          2.69MB byte[] for general heap data2.03MB spring-core-6.1.3.jar                                1.35MB byte[] for embedded resources1.84MB spring-boot-3.2.2.jar                                1.05MB byte[] for reflection metadata894.52kB spring-web-6.1.3.jar                               742.17kB com.oracle.svm.core.hub.DynamicHubCompanion829.04kB jackson-core-2.15.3.jar                            455.69kB c.o.svm.core.hub.DynamicHub$ReflectionMetadata792.90kB spring-beans-6.1.3.jar                             438.81kB java.util.HashMap$Node7.50MB for 69 more packages                                 3.75MB for 3235 more object types
------------------------------------------------------------------------------------------------------------------------

下面我们看打包的结果,进入到项目target目录:
在这里插入图片描述
这里我们看到不但生成了jar包,还有一个可执行文件,如果你是Windows,这里就是一个exe格式的文件。
下面我来运行这个文件,Windows下直接双击运行,macOS下执行:

./target/test-sb-native

运行结果:

2024-01-31T19:44:21.945+08:00  INFO 26199 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2024-01-31T19:44:21.945+08:00  INFO 26199 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 27 ms
2024-01-31T19:44:21.965+08:00  INFO 26199 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 12345 (http) with context path ''
2024-01-31T19:44:21.965+08:00  INFO 26199 --- [           main] o.e.t.TestSbNativeApplication            : Started TestSbNativeApplication in 0.058 seconds (process running for 0.065)

说明运行成功,我们在访问:http://localhost:12345/test

在这里插入图片描述

运行正常。

三、踩坑记录

1、JSON转换问题

下面我们改造一下项目,代码如下:

package org.example.testsbnative;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.io.Serializable;@SpringBootApplication
@RestController
public class TestSbNativeApplication {public static void main(String[] args) {SpringApplication.run(TestSbNativeApplication.class, args);}@RequestMapping("/test")public Object test(){return "hello native";}@RequestMapping("/json")public Object json(){return new User("1","user1");}@Data@NoArgsConstructor@AllArgsConstructorstatic class User implements Serializable{private String id;private String name;}
}

我们增加一个URL,来返回json格式的数据,然后打包并运行,并访问http://localhost:12345/json,发现返回如下错误:

curl http://localhost:12345/json
{"timestamp":"2024-01-31T12:40:40.066+00:00","status":406,"error":"Not Acceptable","path":"/json"}

后台收到这样一个警告:

2024-01-31T20:39:21.449+08:00  WARN 29757 --- [io-12345-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: No acceptable representation]

导致这样的问题,是因为我们返回json需要使用到Java的序列化和反序列化机制,Java的序列化机制是利用的JVM的特性来完成的。

解决方式:在启动类上加一个@RegisterReflectionForBinding(TestSbNativeApplication.User.class)注解,把需要序列化的类全部加入RegisterReflectionForBinding注解中,这里我们加入配置后重新打包并运行,就能正常返回:

curl http://localhost:12345/json
{"id":"1","name":"user1"}

2、反射、资源、jni的调用问题

因为这三个问题的解决方式是一样的,所以这里我们统一来处理,我们先改造一下代码:

@RequestMapping("/rf")
public Object ex() throws Exception{Field roleField = ReflectionUtils.findField(Role.class,"name");assert roleField != null;ReflectionUtils.makeAccessible(roleField);Role role=new Role();roleField.set(role,"role1");Field userField = ReflectionUtils.findField(User.class,"name");assert userField != null;ReflectionUtils.makeAccessible(userField);User user=new User();userField.set(user,"user1");return List.of(role.getName(),user.getName());
}
@RequestMapping("/rs")
public Object rs() throws Exception{try (InputStream inputStream=getClass().getResourceAsStream("/config.properties")){assert inputStream != null;return IOUtils.toByteArray(inputStream);}catch (Exception e){return "发生异常:"+e.getMessage();}
}
@RequestMapping("/oshi")
public Object oshi() throws Exception{StringBuffer buffer=new StringBuffer();buffer.append(OshiUtils.getOs().getFamily());buffer.append(OshiUtils.getSystem().getHardwareUUID());buffer.append(OshiUtils.getSystem().getModel());buffer.append(OshiUtils.getMemory().getAvailable());return buffer;
}

1、我们首先加入对反射的应用

2、加入对额外资源的应用,我们加了一个配置文件

3、我们加入oshi来检测对JNI的应用

我们先打包然后运行,这一切都是正常的,然后我们来测试:

反射:

curl http://localhost:12345/rf
{"timestamp":"2024-02-01T02:05:16.308+00:00","status":500,"error":"Internal Server Error","path":"/rf"}

资源:

curl http://localhost:12345/rs
发生异常:inputStream

JNI调用:

curl http://localhost:12345/oshi
{"timestamp":"2024-02-01T02:07:03.492+00:00","status":500,"error":"Internal Server Error","path":"/oshi"}

全部无法使用,这下完犊子了,我们一个项目不可能不用反射,也不可能不使用其他资源文件,当然jni也是我们常用的东西。下面我们就来解决这个问题。

导致这样的问题,也是GraalVM的特性决定的,关于这方面的解释,大家可以去官网上查看,同时要解决大家也可以参照这里

就是要把需要用到的资源和反射的类都要进行申明,我感觉这种方式不可取,一个项目中要把你所用的所有资源和反射的类都统计出来,貌似很难,而且我们用的外部jar包里面,别人用没用怎么清楚啊。

如果要进行自动统计,这里我们就要使用Java里面的-agent机制,关于agent模式,相关资料我也不多介绍了,我们具体讲解操作,

第一步:我们先将项目进行普通打包

mvn clean -DskipTests package

第二步:使用agent模式来启动jar包

java -agentlib:native-image-agent=config-output-dir=native  -jar target/sb-test.jar

运行这个命令后,会在项目下产生一个native的文件夹,这里就会吧用到的资源,反射,jni的信息全部收集起来。

但这里有个瑕疵,它并不会自动收集,而是需要人工手动来触发,比如我们想要收集刚才的反射用的资源,我们必须手动调用curl http://localhost:12345/rf,让那部分代码执行,agent模式只会收集执行过的代码,对应没有执行过的代码就不会收集。这也是个大坑,这就会要求我们在打包时,必须要保证我们所有的用到这三种技术的代码块都能执行一次,
不然就会漏掉。

第三步:执行代码

为了方便,我们这里写一个Junit的单元,把我们用到过的反射、资源、jni部分的代码保都能执行一次,这个例子比较简单,代码如下:

package org.example.testsbnative;import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;class TestSbNativeApplicationTests {private static HttpClient client = HttpClient.newBuilder().build();@Testvoid rf() throws Exception{System.out.println(get("http://localhost:12345/rf").body());System.out.println(get("http://localhost:12345/rs").body());System.out.println(get("http://localhost:12345/oshi").body());}private static HttpResponse<String> get(String url) throws Exception{URI uri=URI.create(url);HttpRequest.Builder builder=HttpRequest.newBuilder().timeout(Duration.ofSeconds(8)).uri(uri).GET();HttpRequest request = builder.build();return client.send(request, HttpResponse.BodyHandlers.ofString());}
}

或者大家可以手动来执行,执行结果如下:

["role1","user1"]
config1=config1
config2=config2macOS607881B4-CF4B-555B-872C-C7DDAAD9E799MacBookPro16,116471416832

说明程序是没问题,而且在JVM平台下都能正常运行.

第四步:结束agent

agent模式需要我们手动结束,直接按ctrl+c,然后我们检查项目目录下就产生了一个native文件夹:
在这里插入图片描述
大家可以打开看下里面的内容

第五步:进行native打包

在进行native打包的时候,我们需要修改插件的配置:

<plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><configuration><mainClass>org.example.testsbnative.TestSbNativeApplication</mainClass><agentResourceDirectory>${basedir}/native</agentResourceDirectory><imageName>sb-native</imageName><fallback>false</fallback><verbose>true</verbose><quickBuild>true</quickBuild><metadataRepository><enabled>true</enabled></metadataRepository></configuration>
</plugin>

修改完成后执行命令:

mvn clean -DskipTests native:compile -Pnative

打包成功,然后启动运行,再来测试这三个接口:

反射测试:

curl http://localhost:12345/rf  
["role1","user1"]

资源文件测试:

curl http://localhost:12345/rs  
config1=config1
config2=config2

jni测试:

curl http://localhost:12345/oshi
macOS607881B4-CF4B-555B-872C-C7DDAAD9E799MacBookPro16,116963862528

最终发现,一切正常

3、HTTPS调用问题

如果在我们的项目中需要调用外部的https接口,需要在编译时加入–enable-url-protocols参数,具体配置如下:

<buildArgs><arg>--enable-url-protocols=http,https</arg>
</buildArgs>

大家可以自行测试一下

4、Linux下CPU架构问题

默认情况下GraalVM打包对CPU架构的支持采用native模式,就是如果我是在AMD64架构的机器上编译,那编译的程序就只能在AMD64的CPU上运行,在AArch64上编译的就只能在AArch64的CPU上运行,这给跨平台带来很大不便,要解决这个方案加入下面配置:

<buildArgs><arg>--enable-url-protocols=http,https</arg><arg>-march=compatibility</arg>
</buildArgs>

改成兼容模式,经过测试,基本上没什么问题

5、Linux下GLIBC版本的问题

这里最明显的例子就是,我再centos7上面编译的程序,然后放到centos6上去运行,结果出现下面的错误:

./sb-native: /lib64/libc.so.6: version `GLIBC_2.15' not found (required by ./sb-native)
./sb-native: /lib64/libc.so.6: version `GLIBC_2.14' not found (required by ./sb-native)

具体原因是centos6上的GLIBC版本过低导致,要解决这个问题,可以下载我这里的的补丁文件,进行逐个安装后即可

6、部分Windows系统无法缺少相关的库文件

在部分Windows服务器上,运行本地包时,会报找不到XXXX,这是因为缺少相关的库,点击这里下载补丁,双击安装即可。

经过测试,大部分Windows操作系统都能正常运行,但唯有win7是个例外。应该是绝大部分win7都无法运行,目前还没找到原因,我甚至用go打包后的执行文件,在win7上都无法运行。


总结

1、总的来说GraalVM目前还不是很成熟,要想达到c/c++/go那样的编译效果,还差的很远。

2、对应 反射、资源、jni的调用问题的解决方式Java agent是一种解决方式,另外也可以使用springboot提供的注解来解决,但是这样要自己去枚举项目中所用到的所有的资源和反射的类,具体的注解可以参照:@ImportRuntimeHints、@RegisterReflectionForBinding

3、但相信GraalVM会越来越完善,毕竟这对Java开发者来说,编译二进制本地程序已经没被卡脖子了。

4、对应比较大或者业务逻辑比较复杂的Java项目,建议不要尝试GraalVM,这里面的坑估计踩不完。

5、用GraalVM编译的程序,在CPU占用和内存占用相对在JVM平台上来说,真的是指数级的提高,后面我会给大家分享相关的测试。

6、由于是编译本机二进制,所以失去了跨平台特性,Java的一次编译到处运行的优势不再。比如我想在Windows下运行,那我必须要到Windows下去编译才行。

7、目前比较通用的做法是在docker下编译,后面我给大家分享在docker如何编译。

这篇关于在maven环境中使用GraalVM来构建本地原生应用程序(一)构建本地可执行文件的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/681198

相关文章

vue使用docxtemplater导出word

《vue使用docxtemplater导出word》docxtemplater是一种邮件合并工具,以编程方式使用并处理条件、循环,并且可以扩展以插入任何内容,下面我们来看看如何使用docxtempl... 目录docxtemplatervue使用docxtemplater导出word安装常用语法 封装导出方

Linux换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将

使用Jackson进行JSON生成与解析的新手指南

《使用Jackson进行JSON生成与解析的新手指南》这篇文章主要为大家详细介绍了如何使用Jackson进行JSON生成与解析处理,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 核心依赖2. 基础用法2.1 对象转 jsON(序列化)2.2 JSON 转对象(反序列化)3.

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

Elasticsearch 在 Java 中的使用教程

《Elasticsearch在Java中的使用教程》Elasticsearch是一个分布式搜索和分析引擎,基于ApacheLucene构建,能够实现实时数据的存储、搜索、和分析,它广泛应用于全文... 目录1. Elasticsearch 简介2. 环境准备2.1 安装 Elasticsearch2.2 J

使用C#代码在PDF文档中添加、删除和替换图片

《使用C#代码在PDF文档中添加、删除和替换图片》在当今数字化文档处理场景中,动态操作PDF文档中的图像已成为企业级应用开发的核心需求之一,本文将介绍如何在.NET平台使用C#代码在PDF文档中添加、... 目录引言用C#添加图片到PDF文档用C#删除PDF文档中的图片用C#替换PDF文档中的图片引言在当

Java中List的contains()方法的使用小结

《Java中List的contains()方法的使用小结》List的contains()方法用于检查列表中是否包含指定的元素,借助equals()方法进行判断,下面就来介绍Java中List的c... 目录详细展开1. 方法签名2. 工作原理3. 使用示例4. 注意事项总结结论:List 的 contain

C#使用SQLite进行大数据量高效处理的代码示例

《C#使用SQLite进行大数据量高效处理的代码示例》在软件开发中,高效处理大数据量是一个常见且具有挑战性的任务,SQLite因其零配置、嵌入式、跨平台的特性,成为许多开发者的首选数据库,本文将深入探... 目录前言准备工作数据实体核心技术批量插入:从乌龟到猎豹的蜕变分页查询:加载百万数据异步处理:拒绝界面

Android中Dialog的使用详解

《Android中Dialog的使用详解》Dialog(对话框)是Android中常用的UI组件,用于临时显示重要信息或获取用户输入,本文给大家介绍Android中Dialog的使用,感兴趣的朋友一起... 目录android中Dialog的使用详解1. 基本Dialog类型1.1 AlertDialog(

Python使用自带的base64库进行base64编码和解码

《Python使用自带的base64库进行base64编码和解码》在Python中,处理数据的编码和解码是数据传输和存储中非常普遍的需求,其中,Base64是一种常用的编码方案,本文我将详细介绍如何使... 目录引言使用python的base64库进行编码和解码编码函数解码函数Base64编码的应用场景注意