在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

相关文章

Java使用Thumbnailator库实现图片处理与压缩功能

《Java使用Thumbnailator库实现图片处理与压缩功能》Thumbnailator是高性能Java图像处理库,支持缩放、旋转、水印添加、裁剪及格式转换,提供易用API和性能优化,适合Web应... 目录1. 图片处理库Thumbnailator介绍2. 基本和指定大小图片缩放功能2.1 图片缩放的

Win10安装Maven与环境变量配置过程

《Win10安装Maven与环境变量配置过程》本文介绍Maven的安装与配置方法,涵盖下载、环境变量设置、本地仓库及镜像配置,指导如何在IDEA中正确配置Maven,适用于Java及其他语言项目的构建... 目录Maven 是什么?一、下载二、安装三、配置环境四、验证测试五、配置本地仓库六、配置国内镜像地址

Python使用Tenacity一行代码实现自动重试详解

《Python使用Tenacity一行代码实现自动重试详解》tenacity是一个专为Python设计的通用重试库,它的核心理念就是用简单、清晰的方式,为任何可能失败的操作添加重试能力,下面我们就来看... 目录一切始于一个简单的 API 调用Tenacity 入门:一行代码实现优雅重试精细控制:让重试按我

MySQL中EXISTS与IN用法使用与对比分析

《MySQL中EXISTS与IN用法使用与对比分析》在MySQL中,EXISTS和IN都用于子查询中根据另一个查询的结果来过滤主查询的记录,本文将基于工作原理、效率和应用场景进行全面对比... 目录一、基本用法详解1. IN 运算符2. EXISTS 运算符二、EXISTS 与 IN 的选择策略三、性能对比

SpringBoot多环境配置数据读取方式

《SpringBoot多环境配置数据读取方式》SpringBoot通过环境隔离机制,支持properties/yaml/yml多格式配置,结合@Value、Environment和@Configura... 目录一、多环境配置的核心思路二、3种配置文件格式详解2.1 properties格式(传统格式)1.

使用Python构建智能BAT文件生成器的完美解决方案

《使用Python构建智能BAT文件生成器的完美解决方案》这篇文章主要为大家详细介绍了如何使用wxPython构建一个智能的BAT文件生成器,它不仅能够为Python脚本生成启动脚本,还提供了完整的文... 目录引言运行效果图项目背景与需求分析核心需求技术选型核心功能实现1. 数据库设计2. 界面布局设计3

使用IDEA部署Docker应用指南分享

《使用IDEA部署Docker应用指南分享》本文介绍了使用IDEA部署Docker应用的四步流程:创建Dockerfile、配置IDEADocker连接、设置运行调试环境、构建运行镜像,并强调需准备本... 目录一、创建 dockerfile 配置文件二、配置 IDEA 的 Docker 连接三、配置 Do

Android Paging 分页加载库使用实践

《AndroidPaging分页加载库使用实践》AndroidPaging库是Jetpack组件的一部分,它提供了一套完整的解决方案来处理大型数据集的分页加载,本文将深入探讨Paging库... 目录前言一、Paging 库概述二、Paging 3 核心组件1. PagingSource2. Pager3.

深入浅出SpringBoot WebSocket构建实时应用全面指南

《深入浅出SpringBootWebSocket构建实时应用全面指南》WebSocket是一种在单个TCP连接上进行全双工通信的协议,这篇文章主要为大家详细介绍了SpringBoot如何集成WebS... 目录前言为什么需要 WebSocketWebSocket 是什么Spring Boot 如何简化 We

python使用try函数详解

《python使用try函数详解》Pythontry语句用于异常处理,支持捕获特定/多种异常、else/final子句确保资源释放,结合with语句自动清理,可自定义异常及嵌套结构,灵活应对错误场景... 目录try 函数的基本语法捕获特定异常捕获多个异常使用 else 子句使用 finally 子句捕获所