系列文章目录

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



文章目录

  • 系列文章目录
  • 在maven环境中使用GraalVM来构建本地原生应用程序(一)构建本地可执行文件
  • 前言
  • 二、使用docker构建
  • 1、对资源、反射的另一种处理方式
  • 2、打包测试
  • 3、使用docker编译
  • 4、测试镜像
  • 5、native程序性能测试
  • 总结



前言

在上一节中,我们分享了如何构建本地的执行文件,如果你的程序要在多平台下运行,就必须得到对应的操作系统上去编译,这是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

然后观察任务列表:

docker 中 maven 的镜像地址 maven打包docker镜像_maven

我们可以看到CPU的使用和内存使用情况,下面我们编译成native程序再来看看效果:

docker 中 maven 的镜像地址 maven打包docker镜像_spring boot_02

我们可以看到内存的使用是低了很多。如果在Linux下应该效果会更好。大家可以自己实践体验。


总结

1、GraalVM可挖掘的东西还很多,我分享的这些东西也只是皮毛,希望大家共同去探索交流,因为GraalVM确实能解决一些性能问题。

2、至于是原生还是JVM需要根据项目的需求而定,所有的模式都是可选方案。

3、GraalVM也能制作出动态库,供其他语言来使用,目前我们测试了c/c++基本没问题。