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.
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 支持
-
准备工作
环境准备:
-
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
-
-
-
代码编写
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); } }
-
启动对比验证
- 启动时间对比
JVM 运行:maven 的profiles 不要勾选native 然后在IDE 启动应用
Native模式运行:选择maven profile为native 然后点击IDEA 的plugins 中的native:build
编译比较耗时,请耐心等待后。target目录下有一个可执行文件
可执行文件的启动时间非常的短只有0.848s
-
Spring AOT执行代码对比
target目录下的spring-aot的文件夹中存在资源文件的描述
反射的资源文件描述
-
Spring AOT 资源文件查看
target目录下Spring AOT 自动生成的代码查看
- 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)文章来源:https://www.toymoban.com/news/detail-439698.html
-
社区比Spring 更小
Quarkus:
优点:
-
现代云原生框架
-
反应堆
-
最小的内存占用和启动时间
-
基于标准和框架(JAX-RS、Netty、Eclipse Micro profile)
-
Graalvm / Serverless 云功能
-
个人感觉文档支持较为全面,用起来也比较好用
缺点:
-
预览中的多语言支持 (Kotlin Scala)
-
较慢的编译时间 (AOT)
目前Spring AOT 也都是在实验阶段,相对于Quarkus 和Micronaut 来说起步应该比较晚,预计等SpringFramework6 和Spring 3.0 正式版发布之后,有更多的开发者使用起来之后才会发展的更快,Quarkus、Micronaut目前来看支持度较好,不过更看好Quarkus框架,感觉文档更全面一些。现在对云原生框架的探索也仅仅停留在能简单用起来的阶段,国内这部分资料也比较少,后边涉及到微服务这些配套组件的集成还需慢慢探索。需要先会用,才能探究其原理。文章来源地址https://www.toymoban.com/news/detail-439698.html
到了这里,关于Springboot 3.0之Spring Native初体验的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!