GraalVM 原生镜像介绍
GraalVM 原生镜像提供了一种部署和运行 Java 应用程序的新方法。与 Java 虚拟机相比,原生镜像可以以更小的内存占用和更快的启动时间运行。
它们非常适合使用容器镜像部署的应用程序,并且与“函数即服务”(FaaS)平台结合使用时尤其引人注目。
与为 JVM 编写的传统应用程序不同,GraalVM 原生镜像应用程序需要提前处理才能创建可执行文件。此提前处理涉及从其主入口点静态分析您的应用程序代码。
GraalVM 原生镜像是一个完整的、特定于平台的可执行文件。您无需交付 Java 虚拟机即可运行原生镜像。
如果您只想开始并尝试使用 GraalVM,您可以跳转到开发你的第一个 GraalVM 原生应用部分,稍后再返回本节。 |
与 JVM 部署的关键区别
GraalVM 原生镜像是提前生成的,这意味着原生应用程序和基于 JVM 的应用程序之间存在一些关键区别。主要区别在于:
-
在构建时从
main
入口点执行应用程序的静态分析。 -
创建原生镜像时无法访问的代码将被删除,并且不会成为可执行文件的一部分。
-
GraalVM 无法直接识别代码的动态元素,必须告知其反射、资源、序列化和动态代理。
-
应用程序类路径在构建时固定,无法更改。
-
没有延迟类加载,可执行文件中包含的所有内容都将在启动时加载到内存中。
-
Java 应用程序的某些方面存在一些限制,这些方面并未完全支持。
除了这些差异之外,Spring 使用一种称为Spring 提前 (Ahead-of-Time) 处理 的过程,这会带来进一步的限制。请务必阅读下一节的开头部分,以了解这些限制。
GraalVM 参考文档的原生镜像兼容性指南部分提供了关于 GraalVM 限制的更多详细信息。 |
理解 Spring 提前 (Ahead-of-Time) 处理
典型的 Spring Boot 应用程序非常动态,配置是在运行时执行的。事实上,Spring Boot 自动配置的概念很大程度上依赖于对运行时状态的反应,以便正确地配置各项内容。
虽然可以告诉 GraalVM 应用程序的这些动态方面,但这会抵消静态分析的大部分好处。因此,当使用 Spring Boot 创建原生镜像时,会假设一个封闭世界,并且会限制应用程序的动态方面。
封闭世界假设意味着,除了GraalVM 本身造成的限制之外,还存在以下限制:
-
在您的应用程序中定义的 Bean 无法在运行时更改,这意味着:
-
Spring 的
@Profile
注解和特定于配置文件的配置存在限制。 -
如果创建 Bean,则不支持更改的属性(例如,
@ConditionalOnProperty
和.enable
属性)。
-
当这些限制到位时,Spring 就可以在构建时执行提前 (Ahead-of-Time) 处理,并生成 GraalVM 可以使用的附加资源。经过 Spring AOT 处理的应用程序通常会生成:
-
Java 源代码
-
字节码(用于动态代理等)
-
GraalVM JSON 提示文件
-
资源提示 (
resource-config.json
) -
反射提示 (
reflect-config.json
) -
序列化提示 (
serialization-config.json
) -
Java 代理提示 (
proxy-config.json
) -
JNI 提示 (
jni-config.json
)
-
源代码生成
Spring 应用程序由 Spring Bean 组成。在内部,Spring Framework 使用两种不同的概念来管理 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 的 Bean 定义,还有一个myBean
的 Bean 定义。当需要myBean
实例时,会调用BeanInstanceSupplier
。此提供程序将在myConfiguration
Bean 上调用myBean()
方法。
在 Spring AOT 处理期间,您的应用程序会启动到 Bean 定义可用的程度。Bean 实例不会在 AOT 处理阶段创建。 |
Spring AOT 将为所有 Bean 定义生成类似的代码。当需要 Bean 后处理时(例如,调用@Autowired
方法),它还会生成代码。还将生成一个ApplicationContextInitializer
,Spring Boot 将在实际运行 AOT 处理的应用程序时使用它来初始化ApplicationContext
。
虽然 AOT 生成的源代码可能冗长,但它非常易读,并且在调试应用程序时可能会有所帮助。使用 Maven 时,生成的源文件可以在target/spring-aot/main/sources 中找到;使用 Gradle 时,则在build/generated/aotSources 中找到。 |
提示文件生成
除了生成源文件外,Spring AOT 引擎还会生成 GraalVM 使用的提示文件。提示文件包含 JSON 数据,描述 GraalVM 如何处理它无法通过直接检查代码来理解的内容。
例如,您可能在一个私有方法上使用了 Spring 注解。Spring 将需要使用反射来调用私有方法,即使在 GraalVM 上也是如此。在这种情况下,Spring 可以编写反射提示,以便 GraalVM 知道即使没有直接调用私有方法,它仍然需要在原生镜像中可用。
提示文件生成在META-INF/native-image
下,GraalVM 会自动拾取它们。
使用 Maven 时,生成的提示文件可以在target/spring-aot/main/resources 中找到;使用 Gradle 时,则在build/generated/aotResources 中找到。 |