介绍 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 就可以在构建时执行提前处理,并生成 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 框架使用两种不同的概念来管理 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 定义可用为止。在 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 使用的提示文件。提示文件包含 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 目录中找到它们。