Springboot 3.0之Spring Native初体验

Springboot 3.0之Spring Native初体验

Spring 3.0 中引入了一个新特性,即Spring 对Graalvm Image的支持。

Graalvm 官网

https://www.graalvm.org/native-image/

GraalVM编译器

Graalvm 是一个高效能,支持云原生的编译器。支持Java、JavaScript、Python、Ruby、R、WASM等多种语言。编译器的作用就是生成需要更少计算资源的更快、更精简的代码,拿Java 语言举例,Java 代码经过编译后生成class文件,启动Java程序的时候,需要通过JVM虚拟机将class文件加载到JVM内存中运行。现在使用Graalvm 生成Image镜像时,在编译Java代码时会使用 AOT(Ahead-Of-Time),即在编译时直接编译为本机二进制文件,这些文件可立即启动,无需预热即可提供最佳性能。编译完成的二进制文件不需要Java虚拟机即可运行。在不使用Graalvm 的镜像编译功能时,也可以使用Graalvm当作JDK来使用。

Graalvm 架构图[来自官网:https://www.graalvm.org/22.3/docs/introduction/]:

Springboot 3.0之Spring Native初体验_第1张图片

GraalVM为HotSpot Java虚拟机添加了一个高级的即时(JIT)优化编译器,Graalvm 的语言实现框架(Truffle) 可以在JVM上运行JavaScript、Ruby、Python和一些其他支持的流行语言。

Graalvm和JDK的区别:

  • Graalvm 企业对标Oracle JDK,Graalvm 社区版对OpenJDK

  • Graalvm 在基础支持的JDK上又添加了一个高级的JIT编译器,并且这个编译器默认为顶层的JIT编译器,运行时程序正常在JVM加载和执行,编译器将字节码编译为机器码并将其返回JVM时,支持的语言解释器是在Truffle 框架之上编写。

  • Graalvm 支持Native Image,JDK并不支持

  • Graalvm 支持多语言API,即在共享运行中组合编程语言的API(待探究)

Graalvm 目前支持的Java 框架有:

  • Micronaut Java 云原生框架

  • Spring (Spring AOT 插件支持)

  • Helidon (没听过这个)

  • Quarkus Java 云原生框架

Graalvm 目前平台的支持情况:

Community Edition 22.1 by platform.

Feature Linux AMD64 Linux ARM64 macOS macOS ARM64 Windows
Native Image stable stable stable experimental stable
LLVM runtime stable stable stable experimental not available
LLVM toolchain stable stable stable experimental not available
JavaScript stable stable stable experimental stable
Node.js stable stable stable not available stable
Java on Truffle experimental experimental experimental experimental experimental
Python experimental not available experimental not available not available
Ruby experimental experimental experimental experimental not available
R experimental not available experimental not available not available
WebAssembly experimental experimental experimental experimental experimental

JVM 部署模式和原生镜像部署的关键区别

  • 编译为原生镜像时的静态代码分析是从主入口点执行,即 Java 的main方法

  • 无法识别的代码将会被删除,并且不会成为可执行文件的一部分(有点坑)

  • Graalvm 编译时不能识别代码的动态元素,如:JVM的反射机制、Classpath Resource、序列化、动态代理等

  • 应用程序的类路径在生成时是固定的,不能更改

  • 没有所谓的延迟加载(LAZY),所有可执行文件的内容会在程序启动时全部加载到内存中

  • Java 中的一些限制并没有完全受支持

理解Ahead-of-Time

Springboot依赖的就是动态配置很大程度依赖运行时的状态,而Graalvm 在创建NativeImage时,需要在代码编译时对代码进行静态分析,编译成对应的机器码,也就是说,针对于反射、序列化这种依赖于虚拟机的操作,都会被移除。Spring 的Ahead-of-time(AOT插件)就是在代码编译前做一些适配Graalvm的工作,以便Graalvm 能正确解析Springboot的代码,这些提前的工作包括:

  • Spring AOT 生成对应的源代码(需要动态生成的类直接解析生成固定的代码)

  • 字节码的处理,如Spring 中需要动态代理的Bean的处理

  • 依据应用代码生成Graalvm需要的配置文件,告诉Graalvm哪里有反射、资源文件、动态代理等,包括:

    • Resource hints (resource-config.json)

    • Reflection hints (reflect-config.json)

    • Serialization hints (serialization-config.json)

    • Java Proxy Hints (proxy-config.json)

    • JNI Hints (jni-config.json)

以@Configuration 注解举例

@Configuration(proxyBeanMethods = false)
public class MyConfiguration {

    @Bean
    public MyBean myBean() {
        return new MyBean();
    }

}

@Configuration 中配置的@Bean注解,会在程序启动时,由Spring的IOC 容器进行初始化,也就是运行时才创建的Bean对象,当我们创建一个Native image时,Spring就会使用另一种方法去解析这个Bean并创建Bean,Spring AOT 插件会将这个代码做以下处理:

/**
 * Bean definitions for {@link MyConfiguration}.
 */
public class MyConfiguration__BeanDefinitions {

    /**
     * Get the bean definition for 'myConfiguration'.
     */
    public static BeanDefinition getMyConfigurationBeanDefinition() {
        Class<?> beanType = MyConfiguration.class;
        RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
        beanDefinition.setInstanceSupplier(MyConfiguration::new);
        return beanDefinition;
    }

    /**
     * Get the bean instance supplier for 'myBean'.
     */
    private static BeanInstanceSupplier<MyBean> getMyBeanInstanceSupplier() {
        return BeanInstanceSupplier.<MyBean>forFactoryMethod(MyConfiguration.class, "myBean").withGenerator(
                (registeredBean) -> registeredBean.getBeanFactory().getBean(MyConfiguration.class).myBean());
    }

    /**
     * Get the bean definition for 'myBean'.
     */
    public static BeanDefinition getMyBeanBeanDefinition() {
        Class<?> beanType = MyBean.class;
        RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
        beanDefinition.setInstanceSupplier(getMyBeanInstanceSupplier());
        return beanDefinition;
    }

可以看到上边生成的代码创建的MyConfiguration的类与@Configuration创建的类大致等效,区别就在于,SpringAOT插件生成的代码 是以Graalvm编译器能直接识别的方式创建的,在Spring AOT处理期间,并不会创建Bean的实例,而是在启动时创建。

初体验Spirng 3.0 Native-Image 支持

  1. 准备工作

    环境准备:

    • IDEA 2021.3,具体版本自己看,最好是要支持JDK 17的有些低版本的不支持

    • Maven 3.8.1 Maven 版本要和IDEA兼容,有些不兼容,执行Maven命令会报错,Settings.xml配置,可以暂时取消阿里的Maven仓库镜像,不然会导致无法下载Spring maven 仓库的镜像,因为有些SNAPSHOT版本在阿里仓库没有

    • Graalvm 17 (graalvm-ce-java17-22.1.0,担心和本机JDK冲突的,可以直接在IDEA里配置)

      • cd Graalvm 安装目录

      • gu list 验证是否安装native-image
        在这里插入图片描述

      • 没有安装的 执行gu install native-image 命令安装native-image

  2. 代码编写

    HelloService

    public interface HelloService {
    
        String sayHello(String name);
    
        default String sayHello(String prefix,String name){
            return String.format("%s %s",prefix,name);
        }
    }
    
    
     ResourceHelloService
    
    ```java
    public class ResourceHelloService implements HelloService{  
    
      private final Resource resource;  
    
      public ResourceHelloService(Resource resource) {  
    
        this.resource = resource;  
    
      }  
    
        @Override  
    
      public String sayHello(String name) {  
    
        try {  
    
          try(InputStream in = this.resource.getInputStream()){  
    
            String prefix = StreamUtils.copyToString(in, StandardCharsets.UTF_8);  
    
      return sayHello(prefix, name);  
    
      }  
    
        }catch (Exception ex){  
    
          throw new IllegalStateException("Failed to read resource " + null, ex);  
    
      }  
    
        }  
    
    }
    

    SimpleHelloService

    public class SimpleHelloService implements HelloService{
    
        @Override
        public String sayHello(String name) {
            return sayHello("Hello", name);
        }
    }
    

    DemoConfiguration

    @Configuration(proxyBeanMethods = false)
    public class DemoConfiguration {
        @Bean
        HelloService helloService() {
            return new SimpleHelloService();
        }
    }
    

    DemoController

    @RestController
    // 一定到导入
    @ImportRuntimeHints(DemoController.DemoControllerRuntimeHints.class)
    public class DemoController {
    
        private final ObjectProvider<HelloService> helloServices;
    
        public DemoController(ObjectProvider<HelloService> helloServices) {
            this.helloServices = helloServices;
        }
    
        @GetMapping("/hello")
        HelloResponse hello(@RequestParam(required = false) String mode) throws Exception {
            String message = getHelloMessage(mode, "Native");
            return new HelloResponse(message);    }
    
        private String getHelloMessage(String mode, String name) throws Exception {
            if (mode == null) {
                return "No option provided";
            } else if (mode.equals("bean")) {
                HelloService helloService = this.helloServices.getIfUnique();
                return (helloService != null) ? helloService.sayHello(name) : "No Bean found";
            } else if (mode.equals("reflection")) {
                String implementationName = Optional.ofNullable(getDefaultHelloServiceImplementation())
                        .orElse(SimpleHelloService.class.getName());
    
                Class<?> implementationClass = ClassUtils.forName(implementationName, getClass().getClassLoader());
                Method method = implementationClass.getMethod("sayHello", String.class);
                Object instance = BeanUtils.instantiateClass(implementationClass);
                return (String) ReflectionUtils.invokeMethod(method, instance, name);
            }
            else if(mode.equals("resource")){
                ResourceHelloService resourceHelloService = new ResourceHelloService(new ClassPathResource("hello.txt"));
                return resourceHelloService.sayHello(name);
            }
            return "Unknown mode: "+mode;
        }
        public record HelloResponse(String message) {
    
        }
    
        private String getDefaultHelloServiceImplementation() {
    
            return null;
        }
    
        static class DemoControllerRuntimeHints implements RuntimeHintsRegistrar{
        // 注册Spring AOT 运行时解析的配置,此代码会被Spring AOT 识别并处理
            @Override
            public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
                hints.reflection().registerConstructor(SimpleHelloService.class.getConstructors()[0], ExecutableMode.INVOKE)
                        .registerMethod(ReflectionUtils.findMethod(SimpleHelloService.class,"sayHello",String.class),ExecutableMode.INVOKE);
                hints.resources().registerPattern("hello.txt");
            }
        }
    }
    
    

    DemoAotNativeApplication

    @SpringBootApplication
    public class DemoAotNativeApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoAotNativeApplication.class, args);
        }
    
    }
    
    
  3. 启动对比验证

    • 启动时间对比

    JVM 运行:maven 的profiles 不要勾选native 然后在IDE 启动应用

在这里插入图片描述

Native模式运行:选择maven profile为native 然后点击IDEA 的plugins 中的native:build

Springboot 3.0之Spring Native初体验_第2张图片
Springboot 3.0之Spring Native初体验_第3张图片
编译比较耗时,请耐心等待后。target目录下有一个可执行文件
Springboot 3.0之Spring Native初体验_第4张图片

可执行文件的启动时间非常的短只有0.848s

  1. Spring AOT执行代码对比

    target目录下的spring-aot的文件夹中存在资源文件的描述

Springboot 3.0之Spring Native初体验_第5张图片

反射的资源文件描述

Springboot 3.0之Spring Native初体验_第6张图片

  1. Spring AOT 资源文件查看

    target目录下Spring AOT 自动生成的代码查看

Springboot 3.0之Spring Native初体验_第7张图片

  1. Spring 3.0 DEMO-AOT-NATIVE 项目地址:

参考外国程序员小哥snicoll的项目(也是Spring freamwork的开发人员):https://github.com/snicoll/demo-aot-native.git

补充知识:

不同云原生框架之间的对比

Spring /Micronaut/Quarkus 对比

Spring Native:

优点:

  • 完善的框架,

  • 使用 Spring webFlux 的反应式堆栈

  • 最大的社区

  • 更多的集成

  • 多语言支持

缺点:

  • 大量使用反射

  • 启动时间和内存使用不太适合无服务器云功能

  • 仅对 Graalvm 的实验性支持

Micronaut

优点:

  • 现代云原生框架

  • 反应堆

  • 最小的内存占用和启动时间

  • 编译期间不修改字节码

  • 删除所有级别的反射使用

  • Graalvm / 无服务器云功能

  • 多语言支持(Java grovy Kotlin)

  • 类似于Spring

缺点:

  • 较慢的编译时间 (AOT)

  • 社区比Spring 更小

Quarkus:

优点:

  • 现代云原生框架

  • 反应堆

  • 最小的内存占用和启动时间

  • 基于标准和框架(JAX-RS、Netty、Eclipse Micro profile)

  • Graalvm / Serverless 云功能

  • 个人感觉文档支持较为全面,用起来也比较好用

缺点:

  • 预览中的多语言支持 (Kotlin Scala)

  • 较慢的编译时间 (AOT)

目前Spring AOT 也都是在实验阶段,相对于Quarkus 和Micronaut 来说起步应该比较晚,预计等SpringFramework6 和Spring 3.0 正式版发布之后,有更多的开发者使用起来之后才会发展的更快,Quarkus、Micronaut目前来看支持度较好,不过更看好Quarkus框架,感觉文档更全面一些。现在对云原生框架的探索也仅仅停留在能简单用起来的阶段,国内这部分资料也比较少,后边涉及到微服务这些配套组件的集成还需慢慢探索。需要先会用,才能探究其原理。

你可能感兴趣的:(java,随笔,spring,spring,boot,jvm)