Spring 3.0 中引入了一个新特性,即Spring 对Graalvm Image的支持。
Graalvm 官网
https://www.graalvm.org/native-image/
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 企业对标Oracle JDK,Graalvm 社区版对OpenJDK
Graalvm 在基础支持的JDK上又添加了一个高级的JIT编译器,并且这个编译器默认为顶层的JIT编译器,运行时程序正常在JVM加载和执行,编译器将字节码编译为机器码并将其返回JVM时,支持的语言解释器是在Truffle 框架之上编写。
Graalvm 支持Native Image,JDK并不支持
Graalvm 支持多语言API,即在共享运行中组合编程语言的API(待探究)
Micronaut Java 云原生框架
Spring (Spring AOT 插件支持)
Helidon (没听过这个)
Quarkus Java 云原生框架
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 |
编译为原生镜像时的静态代码分析是从主入口点执行,即 Java 的main方法
无法识别的代码将会被删除,并且不会成为可执行文件的一部分(有点坑)
Graalvm 编译时不能识别代码的动态元素,如:JVM的反射机制、Classpath Resource、序列化、动态代理等
应用程序的类路径在生成时是固定的,不能更改
没有所谓的延迟加载(LAZY),所有可执行文件的内容会在程序启动时全部加载到内存中
Java 中的一些限制并没有完全受支持
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的实例,而是在启动时创建。
准备工作
环境准备:
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里配置)
代码编写
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 自动生成的代码查看
参考外国程序员小哥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框架,感觉文档更全面一些。现在对云原生框架的探索也仅仅停留在能简单用起来的阶段,国内这部分资料也比较少,后边涉及到微服务这些配套组件的集成还需慢慢探索。需要先会用,才能探究其原理。