GraalVM 原生镜像简介

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

它们非常适合通过容器镜像部署的应用,尤其在结合“函数即服务”(FaaS)平台时更具吸引力。

与传统 JVM 应用不同,GraalVM 原生镜像应用需要提前进行处理(AOT),以生成可执行文件。 该处理过程会从主入口点对应用代码进行静态分析。

GraalVM 原生镜像是完整的、平台相关的可执行文件。 无需携带 Java 虚拟机即可运行原生镜像。

如果你只想快速体验 GraalVM,可以直接跳转到 开发你的第一个 GraalVM Native 应用程序,稍后再回到本节。

与 JVM 部署的关键区别

GraalVM 原生镜像采用提前处理,因此与基于 JVM 的应用存在一些关键差异。 主要区别包括:

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

  • 创建原生镜像时无法到达的代码会被移除,不会包含在可执行文件中。

  • GraalVM 无法直接感知代码中的动态元素,需显式告知反射、资源、序列化和动态代理相关信息。

  • 应用类路径在构建时固定,无法变更。

  • 启动时会加载所有可执行文件中的内容,不支持延迟类加载。

  • 某些 Java 应用特性存在部分不支持的限制。

此外,Spring 采用了 Spring 提前处理(AOT),带来更多限制。 请务必阅读下一节开头内容以了解这些限制。

原生镜像兼容性指南 提供了更多关于 GraalVM 限制的详细信息。

理解 Spring 提前处理(AOT)

典型的 Spring Boot 应用非常动态,配置在运行时完成。 实际上,Spring Boot 自动配置机制高度依赖于运行时状态以正确配置各项内容。

虽然可以让 GraalVM 感知应用的动态特性,但这样会削弱静态分析带来的优势。 因此,使用 Spring Boot 构建原生镜像时,假定应用为“封闭世界”,并限制其动态特性。

“封闭世界”假设除了 GraalVM 本身的限制 外,还包括以下约束:

  • 应用中定义的 bean 运行时不可变,意味着:

在这些限制下,Spring 可以在构建时进行提前处理,生成 GraalVM 可用的额外资源。 经过 Spring AOT 处理的应用通常会生成:

  • Java 源代码

  • 字节码(如动态代理等)

  • GraalVM JSON 提示文件,位于 META-INF/native-image/{groupId}/{artifactId}/

    • 资源提示(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 类本身创建了 bean 定义。

当需要 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 定义,便于 GraalVM 理解。

myConfigurationmyBean 都有对应的 bean 定义。 当需要 myBean 实例时,会调用 BeanInstanceSupplier,该 supplier 会在 myConfiguration bean 上调用 myBean() 方法。

Spring AOT 处理期间,应用会启动到 bean 定义可用的阶段,但不会创建 bean 实例。

Spring AOT 会为所有 bean 定义生成类似代码。 如需 bean 后处理(如调用 @Autowired 方法),也会生成相应代码。 还会生成 ApplicationContextInitializer,用于在实际运行 AOT 处理应用时由 Spring Boot 初始化 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 打包。

与源代码生成不同,生成的字节码对调试帮助不大。 如需检查 .class 文件内容,可用 javap 等工具,Maven 下路径为 target/spring-aot/main/classes,Gradle 下为 build/generated/aotClasses