• 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/]:

    在这里插入图片描述

    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.

    FeatureLinux AMD64Linux ARM64macOSmacOS ARM64Windows
    Native Imagestablestablestableexperimentalstable
    LLVM runtimestablestablestableexperimentalnot available
    LLVM toolchainstablestablestableexperimentalnot available
    JavaScriptstablestablestableexperimentalstable
    Node.jsstablestablestablenot availablestable
    Java on Truffleexperimentalexperimentalexperimentalexperimentalexperimental
    Pythonexperimentalnot availableexperimentalnot availablenot available
    Rubyexperimentalexperimentalexperimentalexperimentalnot available
    Rexperimentalnot availableexperimentalnot availablenot available
    WebAssemblyexperimentalexperimentalexperimentalexperimentalexperimental
    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();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    @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;
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    可以看到上边生成的代码创建的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);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      
       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);  
      
        }  
      
          }  
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37

      SimpleHelloService

      public class SimpleHelloService implements HelloService{
      
          @Override
          public String sayHello(String name) {
              return sayHello("Hello", name);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

      DemoConfiguration

      @Configuration(proxyBeanMethods = false)
      public class DemoConfiguration {
          @Bean
          HelloService helloService() {
              return new SimpleHelloService();
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

      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");
              }
          }
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57

      DemoAotNativeApplication

      @SpringBootApplication
      public class DemoAotNativeApplication {
      
          public static void main(String[] args) {
              SpringApplication.run(DemoAotNativeApplication.class, args);
          }
      
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    3. 启动对比验证

      • 启动时间对比

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

    在这里插入图片描述

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

    在这里插入图片描述
    在这里插入图片描述
    编译比较耗时,请耐心等待后。target目录下有一个可执行文件
    在这里插入图片描述

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

    1. Spring AOT执行代码对比

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

    在这里插入图片描述

    反射的资源文件描述

    在这里插入图片描述

    1. Spring AOT 资源文件查看

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

    在这里插入图片描述

    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+SSH+MyEclipse+MySQL)
    给MuMu模拟器安装证书
    [附源码]Python计算机毕业设计Django基于web的羽毛球管理系统
    常见的云计算安全问题以及如何解决
    linux下的文件的查找
    UI设计就业前景到底好不好?
    vsCode 【 open in server】让局域网的其他人查看自己的文件
    2023年终总结:为何我们总想把时间填满
    如何高效的分析online.log
  • 原文地址:https://blog.csdn.net/qq_41354631/article/details/127847889