系列文章目录
在maven环境中使用GraalVM来构建本地原生应用程序(一)构建本地可执行文件
文章目录
前言
在上一节中,我们分享了如何构建本地的执行文件,如果你的程序要在多平台下运行,就必须得到对应的操作系统上去编译,这是GraalVM的一个不足之处。但如果我们的程序要在docker下运行,那我们就可以直接使用docker来进行编译。这样就减少了对本地平台的依赖。下面我们来看看如何在docker下编译
二、使用docker构建
1、对资源、反射的另一种处理方式
上一节我们采用了Java agent的方式来处理我们系统中的资源、反射的问题,但是agent模式确定很大,需要手动执行和手动停止,真正的人工智能。这样对自动化打包来说就很麻烦,今天我们换一种方式来处理。
反射处理方式:
首先我们要整理出系统用到反射的类然后使用@RegisterReflectionForBinding注解,上一节我们已经使用这个类。整理之后代码如下:
@RegisterReflectionForBinding({TestSbNativeApplication.User.class,TestSbNativeApplication.Role.class})
资源处理方式
资源也需要我们自己去整理,在项目中用到哪些资源,这要求你对项目必须比较熟悉,以及你使用的第三方jar依赖,他们可能使用的资源也要比较熟悉。比如在这个项目中,我们用到自己的配置文件,另外oshi还有自己的配置问题,关于oshi库大家可以自己去查询相关资料,资源处理,我们来实现RuntimeHintsRegistrar接口:
public static class Hints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("config.properties");
hints.resources().registerPattern("logback.xml");
hints.resources().registerPattern("oshi.properties");
hints.resources().registerPattern("oshi.*.properties");
}
}
这里我们把用到的资源全部注册进去,当然hints也可以注册反射,大家可以自行研究.
然后加载Hints:
@ImportRuntimeHints(TestSbNativeApplication.Hints.class)
改造后的代码如下:
package org.example.testsbnative;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
@SpringBootApplication
@RestController
@RegisterReflectionForBinding({TestSbNativeApplication.User.class,TestSbNativeApplication.Role.class})
@ImportRuntimeHints(TestSbNativeApplication.Hints.class)
public class TestSbNativeApplication {
public static void main(String[] args) {
SpringApplication.run(TestSbNativeApplication.class, args);
}
public static class Hints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("config.properties");
hints.resources().registerPattern("logback.xml");
hints.resources().registerPattern("oshi.properties");
hints.resources().registerPattern("oshi.*.properties");
}
}
@RequestMapping("/test")
public Object test(){
return "hello native";
}
@RequestMapping("/json")
public Object json(){
return new User("1","user1");
}
@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;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
static class User implements Serializable{
private String id;
private String name;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
static class Role implements Serializable{
private String id;
private String name;
}
}
2、打包测试
下面我们来验证一下这种方式能不能行,直接使用打包命令:
mvn clean -DskipTests native:compile -Pnative
编译完成,然后运行:
./target/sb-native
测试三个接口:
curl http://localhost:12345/rf
["role1","user1"]
curl http://localhost:12345/rs
config1=config1
config2=config2
curl http://localhost:12345/oshi
macOS607881B4-CF4B-555B-872C-C7DDAAD9E799MacBookPro16,116616337408
我们看到完全没问题,说明这种方式是可行的,唯一的不足就是需要自己手动去枚举各方的东西。
3、使用docker编译
上面我们解决了资源和反射的问题,这样我们可以不使用agent模式。之前我们讲了GraalVM不能编译跨平台的程序,这样就导致要想得到对应操作系统的程序,就必须要到对应的系统上去编译。如果使用docker我们就只依赖docker的环境,与操作系统无关了。构建docker镜像其实也有两种方式。
方式一
这种就是我在宿主机上编译成功,然后直接将编译好的程序打包成镜像,这个过程相对来说简单,但是还是和操作系统有关,比如我在centos上编译,那么我的docker依赖镜像也必须是centos,主要操作方式如下:
编写Dockerfile文件:
FROM centos:7
LABEL authors="CSDN"
RUN mkdir -p /app
WORKDIR /app
COPY target/sb-native /app/sb-native
CMD ["/app/sb-native"]
然后运行native打包:
mvn clean -DskipTests native:compile -Pnative
最后执行docker:
docker build -t sb-native:1.0 .
方式二(推荐)
方式一对操作系统还是有依赖,docker镜像的依赖镜像必须和编译代码的宿主机一致,不然程序就会有问题。方式二我们采用完全只依赖docker环境的方式,具体操作如下:
还是我们需要编写Dockerfile内容如下:
# First stage: 构建环境
FROM ghcr.io/graalvm/graalvm-community:21 AS build
RUN microdnf update -y && \
microdnf install -y maven gcc glibc-devel zlib-devel libstdc++-devel gcc-c++ && \
microdnf clean all
WORKDIR /usr/src/app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY . .
RUN mvn clean -DskipTests -Pnative native:compile
# Second stage: 构建镜像
FROM debian:bookworm-slim
WORKDIR /app
COPY --from=build /usr/src/app/target/sb-native /app/sb-native
CMD ["/app/sb-native"]
然后我们执行docker构建命令:
docker build -t sb-native:1.0 .
初次执行需要等待一会儿,如果看到如下结果表示已经构建完成:
=> [stage-1 1/3] FROM docker.io/library/debian:bookworm-slim@sha256:7802002798b0e351323ed2357ae6dc5a8c4d0a05a57e7f4d8f97136151d3d603 0.0s
=> CACHED [stage-1 2/3] WORKDIR /app 0.0s
=> [build 2/7] RUN microdnf update -y && microdnf install -y maven gcc glibc-devel zlib-devel libstdc++-devel gcc-c++ && microdnf clean all 150.1s
=> [build 3/7] WORKDIR /usr/src/app 0.0s
=> [build 4/7] COPY pom.xml . 0.0s
=> [build 5/7] RUN mvn dependency:go-offline 740.2s
=> [build 6/7] COPY . . 0.0s
=> [build 7/7] RUN mvn clean -DskipTests -Pnative native:compile 159.2s
=> [stage-1 3/3] COPY --from=build /usr/src/app/target/sb-native /app/sb-native 0.2s
=> exporting to image 0.4s
=> => exporting layers 0.4s
=> => writing image sha256:0d28bf7b5104ba82eb227748b7f28f1a362971062ed5e887b0a3383eaa20ab92 0.0s
=> => naming to docker.io/library/sb-native:1.0
然后我们查看docker镜像:
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
sb-native 1.0 0d28bf7b5104 28 seconds ago 162MB
可以看到我们的镜像已经构建完成。
4、测试镜像
镜像构建成功后,我们来测试镜像,运行如下命令启动容器:
docker run \
--name=sb-native \
-itd \
-p 12345:12345 \
--restart=always \
sb-native:1.0
然后检查容器:
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2b0efc4b3d43 sb-native:1.0 "/app/sb-native" 4 seconds ago Up 3 seconds 0.0.0.0:12345->12345/tcp sb-native
最后再来测试三个接口:
curl http://localhost:12345/rf
["role1","user1"]
curl http://localhost:12345/rs
config1=config1
config2=config2
curl http://localhost:12345/oshi
Debian GNU/Linux40464ed3-0000-0000-aa04-7fbfc0dd55c6BHYVE (version: 1.0)7276670976
说明这个镜像是没有问题的,可以看出,使用docker来构建镜像,我们根本不依赖本机环境,只要有docker就可以完成编译。那么编译后的镜像我们这么发布,可以关注我另外的文章,会有详细的介绍。
5、native程序性能测试
上面我们完成的Java native程序的本地打包和镜像构建,那么native程序到底有什么好处呢,下面我们在Windows下来简单测试一下,为了给程序增加点压力,我们加入如下代码:
private static AtomicLong atomicLong=new AtomicLong(0);
@EventListener
void event(ApplicationReadyEvent event) {
for (int i = 0; i < 30; i++) {
new Thread(()->{
while (true){
List<User> users=new ArrayList<>();
for (int j = 0; j < 5000; j++) {
users.add(new User("id-"+j,"name-"+j));
}
long rs=atomicLong.getAndIncrement();
System.out.println(Thread.currentThread().getName()+"--rs=="+rs);
try {
TimeUnit.MICROSECONDS.sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
}
}
}).start();
}
}
我们在程序启动后,运行30个线程,这些线程来对一个静态数据进行修改。
首先我们运行普通打包,在JVM上来运行:
mvn clean -DskipTests package
这里生成,sb-test.jar文件,然后我们用Java来运行:
java -jar target/sb-test.jar
然后观察任务列表:
我们可以看到CPU的使用和内存使用情况,下面我们编译成native程序再来看看效果:
我们可以看到内存的使用是低了很多。如果在Linux下应该效果会更好。大家可以自己实践体验。
总结
1、GraalVM可挖掘的东西还很多,我分享的这些东西也只是皮毛,希望大家共同去探索交流,因为GraalVM确实能解决一些性能问题。
2、至于是原生还是JVM需要根据项目的需求而定,所有的模式都是可选方案。
3、GraalVM也能制作出动态库,供其他语言来使用,目前我们测试了c/c++基本没问题。