介绍 GraalVM 原生镜像
GraalVM 原生镜像为部署和运行 Java 应用程序提供了一种新的方式。与 Java 虚拟机相比,原生镜像可以在更小的内存占用下运行,并且启动时间更快。
它们非常适合使用容器镜像部署的应用程序,在与“功能即服务”(FaaS)平台结合使用时尤其引人注目。
与传统为 JVM 编写的应用程序不同,GraalVM Native Image 应用程序需要进行提前处理以创建可执行文件。这种提前处理涉及从应用程序的主入口点对代码进行静态分析。
GraalVM Native Image 是一个完整的、平台特定的可执行文件。你不需要附带 Java 虚拟机来运行一个原生镜像。
如果您只想快速入门并尝试 GraalVM,可以直接跳转到 开发您的第一个 GraalVM 原生应用 部分,稍后再回到此部分。
与 JVM 部署的主要区别
GraalVM 原生镜像提前生成的事实意味着原生应用与基于 JVM 的应用之间存在一些关键差异。主要差异包括:
-
您的应用程序的静态分析在构建时从
main
入口点执行。 -
在创建原生镜像时无法访问的代码将被移除,不会成为可执行文件的一部分。
-
GraalVM 不会直接识别代码中的动态元素,必须明确告知关于反射、资源、序列化和动态代理的内容。
-
应用程序的类路径在构建时是固定的,无法更改。
-
没有延迟类加载,可执行文件中包含的所有内容都将在启动时加载到内存中。
-
Java 应用程序的某些方面存在一些限制,尚未得到完全支持。
除了这些差异之外,Spring 还使用了一个称为 Spring Ahead-of-Time 处理的过程,这进一步带来了一些限制。请确保至少阅读下一节的开头部分,以了解这些限制。
原生镜像兼容性指南 部分的 GraalVM 参考文档提供了关于 GraalVM 限制的更多详细信息。
理解 Spring 的提前处理
典型的 Spring Boot 应用程序非常动态,配置在运行时进行。事实上,Spring Boot 自动配置的概念在很大程度上依赖于对运行时状态的反应,以便正确配置事物。
尽管向 GraalVM 告知应用程序的这些动态特性是可行的,但这样做将抵消静态分析的大部分优势。因此,在使用 Spring Boot 创建原生镜像时,假设了一个封闭世界,并对应用程序的动态特性进行了限制。
在封闭世界假设下,除了 GraalVM 自身带来的限制 之外,还存在以下限制:
-
应用程序中定义的 Bean 在运行时无法更改,这意味着:
-
不支持因 Bean 创建而改变的属性(例如 @ConditionalOnProperty 和
.enabled
属性)。
当这些限制生效时,Spring 就可以在构建时进行提前处理,并生成 GraalVM 可以使用的额外资源。经过 Spring AOT 处理的应用程序通常会生成:
-
Java 源代码
-
字节码(用于动态代理等)
-
在
META-INF/native-image/{groupId}/{artifactId}/
目录下的 GraalVM JSON 提示文件:-
资源提示 (
resource-config.json
) -
反射提示 (
reflect-config.json
) -
序列化提示 (
serialization-config.json
) -
Java 代理提示 (
proxy-config.json
) -
JNI 提示 (
jni-config.json
)
-
如果生成的提示信息不够充分,您也可以提供自己的提示。
源代码生成
Spring 应用程序由 Spring Bean 组成。在内部,Spring 框架使用两个不同的概念来管理 Bean。首先是 Bean 实例,这些是已经创建并可以注入到其他 Bean 中的实际实例。其次是 Bean 定义,它们用于定义 Bean 的属性以及如何创建其实例。
如果我们以一个典型的 @Configuration 类为例:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class MyConfiguration {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
Bean 定义是通过解析 @Configuration 类并查找其中的 @Bean 方法创建的。在上面的示例中,我们为名为 myBean
的单例 Bean 定义了一个 BeanDefinition。我们还为 MyConfiguration
类本身创建了一个 BeanDefinition。
当需要 myBean
实例时,Spring 知道它必须调用 myBean()
方法并使用其结果。在 JVM 上运行时,应用程序启动时会解析 @Configuration
类,并且 @Bean
方法会通过反射被调用。
在创建原生镜像时,Spring 的工作方式有所不同。它不是在运行时解析 @Configuration 类并生成 bean 定义,而是在构建时完成这些操作。一旦发现了 bean 定义,它们会被处理并转换为源代码,这些源代码可以被 GraalVM 编译器分析。
Spring AOT 过程会将上面的配置类转换为如下代码:
import org.springframework.beans.factory.aot.BeanInstanceSupplier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
/**
* 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;
}
}
生成的代码可能会根据你的 bean 定义的性质而有所不同。
如上所示,生成的代码创建了与 @Configuration 类等效的 Bean 定义,但以一种 GraalVM 可以直接理解的方式实现。
有一个 myConfiguration
bean 的定义,还有一个 myBean
的定义。当需要一个 myBean
实例时,会调用 BeanInstanceSupplier。这个 supplier 会调用 myConfiguration
bean 上的 myBean()
方法。
在 Spring AOT 处理期间,您的应用程序会启动到 bean 定义可用的阶段。在 AOT 处理阶段不会创建 bean 实例。
Spring AOT 会为所有的 Bean 定义生成类似的代码。当需要进行 Bean 后处理时(例如,调用 @Autowired 方法),它也会生成相应的代码。此外,还会生成一个 ApplicationContextInitializer,Spring Boot 会在实际运行经过 AOT 处理的应用程序时使用它来初始化 ApplicationContext。
尽管 AOT 生成的源码可能较为冗长,但它相当易读,并且在调试应用程序时非常有用。生成的源文件在使用 Maven 时可以在 target/spring-aot/main/sources
中找到,而在使用 Gradle 时则位于 build/generated/aotSources
中。
提示文件生成
除了生成源文件外,Spring AOT 引擎还会生成 GraalVM 使用的提示文件(hint files)。提示文件中包含 JSON 数据,这些数据描述了 GraalVM 应如何处理那些它无法通过直接检查代码理解的内容。
例如,你可能在私有方法上使用了 Spring 注解。Spring 需要使用反射来调用私有方法,即使在 GraalVM 上也是如此。当这种情况发生时,Spring 可以编写一个反射提示,以便 GraalVM 知道,即使私有方法没有被直接调用,它仍然需要在原生镜像中可用。
提示文件会在 META-INF/native-image
目录下生成,GraalVM 会自动识别这些文件。
使用 Maven 时,生成的提示文件可以在 target/spring-aot/main/resources
目录下找到;使用 Gradle 时,可以在 build/generated/aotResources
目录下找到。
代理类生成
有时候,Spring 需要生成代理类来为你编写的代码增强额外的功能。为此,它使用了 cglib 库,该库直接生成字节码。
当一个应用程序在 JVM 上运行时,代理类会在应用程序运行时动态生成。在创建原生镜像时,这些代理需要在构建时生成,以便 GraalVM 能够将它们包含在内。
与源代码生成不同,生成的字节码在调试应用程序时并不是特别有帮助。然而,如果你需要使用诸如 javap
这样的工具来检查 .class
文件的内容,你可以在 Maven 的 target/spring-aot/main/classes
目录或 Gradle 的 build/generated/aotClasses
目录中找到它们。