文章目录
- 前言
- 一、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 metadata
5.16MB tomcat-embed-core-10.1.18.jar 3.83MB byte[] for java.lang.String
4.66MB svm.jar (Native Image) 2.95MB java.lang.Class
3.90MB java.xml 2.92MB java.lang.String
2.42MB jackson-databind-2.15.3.jar 2.69MB byte[] for general heap data
2.03MB spring-core-6.1.3.jar 1.35MB byte[] for embedded resources
1.84MB spring-boot-3.2.2.jar 1.05MB byte[] for reflection metadata
894.52kB spring-web-6.1.3.jar 742.17kB com.oracle.svm.core.hub.DynamicHubCompanion
829.04kB jackson-core-2.15.3.jar 455.69kB c.o.svm.core.hub.DynamicHub$ReflectionMetadata
792.90kB spring-beans-6.1.3.jar 438.81kB java.util.HashMap$Node
7.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
@AllArgsConstructor
static 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();
@Test
void 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=config2
macOS607881B4-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如何编译。