GraalVM 原生镜像简介

GraalVM 原生镜像提供了一种部署和运行 Java 应用的新方式。与 Java 虚拟机相比,原生镜像占用的内存更少,启动速度更快。

它们非常适合使用容器镜像部署的应用,并且与“函数即服务”(FaaS)平台结合使用时尤为有趣。

与为 JVM 编写的传统应用不同,GraalVM 原生镜像应用需要提前处理才能创建可执行文件。这种提前处理包括从主入口点对你的应用代码进行静态分析。

GraalVM 原生镜像是一个完整的、特定于平台的可执行文件。运行原生镜像不需要携带 Java 虚拟机。

如果你只想快速入门并尝试 GraalVM,可以跳到“开发你的第一个 GraalVM 原生应用”部分,稍后返回此部分。

与 JVM 部署的主要区别

GraalVM 原生镜像提前生成意味着原生应用和基于 JVM 的应用之间存在一些关键区别。主要区别包括

  • 从主入口点在构建时对你的应用进行静态分析。

  • 在创建原生镜像时无法到达的代码将被移除,并且不会成为可执行文件的一部分。

  • GraalVM 不能直接感知代码中的动态元素,必须告知它有关反射、资源、序列化和动态代理的信息。

  • 应用类路径在构建时固定,不能更改。

  • 没有延迟类加载,可执行文件中包含的所有内容将在启动时加载到内存中。

  • Java 应用的某些方面存在一些局限性,并非完全支持。

除了这些区别之外,Spring 还使用一种称为Spring 提前(AOT)处理的机制,这会施加进一步的限制。请务必阅读下一部分的开头,以了解这些内容。

GraalVM 参考文档的“原生镜像兼容性指南”部分提供了有关 GraalVM 限制的更多详细信息。

理解 Spring 提前(AOT)处理

典型的 Spring Boot 应用非常动态,配置在运行时执行。事实上,Spring Boot 自动配置的概念很大程度上依赖于对运行时状态的响应来正确配置各项内容。

虽然可以将应用的这些动态方面告知 GraalVM,但这样做会抵消大部分静态分析的好处。因此,在使用 Spring Boot 创建原生镜像时,假定为封闭世界(closed-world),并限制应用的动态方面。

封闭世界的假设意味着,除了GraalVM 本身造成的限制之外,还存在以下限制

  • 你的应用中定义的 Bean 不能在运行时更改,这意味着

当这些限制到位后,Spring 就有可能在构建时执行提前(AOT)处理,并生成 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 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();
	}

}

通过解析@Configuration 类并查找@Bean 方法来创建 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 定义性质而异。

你可以看到,生成的代码创建的 Bean 定义与@Configuration 类等价,但以 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 注解。为了调用私有方法,即使在 GraalVM 上,Spring 也需要使用反射。当出现这种情况时,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 找到它们。