此版本仍在开发中,尚未视为稳定版。如需最新稳定版本,请使用 Spring Boot 4.0.4spring-doc.cadn.net.cn

引入 GraalVM 原生镜像

GraalVM 原生镜像为部署和运行 Java 应用程序提供了一种新方式。 与 Java 虚拟机相比,原生镜像可以以更小的内存占用和快得多的启动时间运行。spring-doc.cadn.net.cn

它们非常适合使用容器镜像部署的应用程序,尤其在与“函数即服务”(FaaS)平台结合使用时更具吸引力。spring-doc.cadn.net.cn

与为 JVM 编写的传统应用程序不同,GraalVM Native Image 应用程序需要提前处理(ahead-of-time processing)才能生成可执行文件。 这种提前处理涉及从应用程序的主入口点对其代码进行静态分析。spring-doc.cadn.net.cn

GraalVM 原生镜像是一个完整的、特定于平台的可执行文件。 要运行原生镜像,您无需附带 Java 虚拟机。spring-doc.cadn.net.cn

如果你只是想快速上手并尝试 GraalVM,可以直接跳转到开发你的第一个 GraalVM 原生应用部分,稍后再返回本节。

与 JVM 部署的关键区别

GraalVM 原生镜像是提前生成的,这一事实意味着原生应用与基于 JVM 的应用之间存在一些关键差异。 主要差异包括:spring-doc.cadn.net.cn

  • 在构建时,会从 main 入口点对您的应用程序进行静态分析。spring-doc.cadn.net.cn

  • 在创建原生镜像时无法到达的代码将被移除,并且不会包含在可执行文件中。spring-doc.cadn.net.cn

  • GraalVM 无法直接感知代码中的动态元素,必须显式告知它有关反射、资源、序列化和动态代理的信息。spring-doc.cadn.net.cn

  • 应用程序的类路径在构建时即已确定,无法更改。spring-doc.cadn.net.cn

  • 没有懒加载类机制,所有包含在可执行文件中的内容都会在启动时加载到内存中。spring-doc.cadn.net.cn

  • Java 应用程序的某些方面存在一些限制,这些方面并未得到完全支持。spring-doc.cadn.net.cn

除了上述差异之外,Spring 还使用了一种称为Spring 提前编译(Ahead-of-Time)处理的流程,这会带来进一步的限制。 请务必至少阅读下一节的开头部分,以了解这些限制。spring-doc.cadn.net.cn

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

理解 Spring 的提前处理(Ahead-of-Time Processing)

典型的 Spring Boot 应用程序具有很强的动态性,其配置是在运行时完成的。 事实上,Spring Boot 自动配置的概念在很大程度上依赖于对运行时状态的响应,以便正确地进行配置。spring-doc.cadn.net.cn

尽管可以向 GraalVM 告知应用程序的这些动态特性,但这样做会抵消静态分析带来的大部分优势。 因此,在使用 Spring Boot 创建原生镜像时,会假定一个封闭世界(closed-world),并对应用程序的动态特性加以限制。spring-doc.cadn.net.cn

封闭世界假设(closed-world assumption)除了GraalVM 自身带来的限制之外,还意味着以下限制:spring-doc.cadn.net.cn

当这些限制生效时,Spring 就可以在构建期间执行预先处理(ahead-of-time processing),并生成 GraalVM 可以使用的额外资源。 经过 Spring AOT 处理的应用程序通常会生成:spring-doc.cadn.net.cn

如果生成的提示信息不够充分,你也可以提供自己的提示spring-doc.cadn.net.cn

源代码生成

Spring 应用程序由 Spring Bean 组成。 在内部,Spring 框架使用两个不同的概念来管理 Bean。 一种是 Bean 实例,即实际已创建的实例,可以被注入到其他 Bean 中。 另一种是 Bean 定义,用于定义 Bean 的属性以及其实例应如何创建。spring-doc.cadn.net.cn

如果我们采用一个典型的 @Configuration 类:spring-doc.cadn.net.cn

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 类本身创建一个 BeanDefinitionspring-doc.cadn.net.cn

当需要 myBean 实例时,Spring 知道它必须调用 myBean() 方法并使用其结果。 在 JVM 上运行时,@Configuration 类解析会在应用程序启动时发生,并且 @Bean 方法会通过反射被调用。spring-doc.cadn.net.cn

在创建原生镜像时,Spring 的运行方式有所不同。 它不是在运行时解析 @Configuration 类并生成 Bean 定义,而是在构建时完成这一过程。 一旦发现 Bean 定义,它们将被处理并转换为源代码,以便由 GraalVM 编译器进行分析。spring-doc.cadn.net.cn

Spring AOT 过程会将上述配置类转换为类似下面的代码:spring-doc.cadn.net.cn

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 能够直接理解的方式。spring-doc.cadn.net.cn

存在一个针对 myConfiguration Bean 的定义,以及一个针对 myBean 的定义。 当需要 myBean 实例时,将调用 BeanInstanceSupplier。 此供应器将在 myConfiguration Bean 上调用 myBean() 方法。spring-doc.cadn.net.cn

在 Spring AOT 处理期间,您的应用程序会启动到 Bean 定义可用的阶段。 在 AOT 处理阶段不会创建 Bean 实例。

Spring AOT 将为您的所有 Bean 定义生成如下代码。 当需要 Bean 后处理时(例如,调用 @Autowired 方法),它也会生成相应代码。 此外,还会生成一个 ApplicationContextInitializer,供 Spring Boot 在实际运行经过 AOT 处理的应用程序时,用于初始化 ApplicationContextspring-doc.cadn.net.cn

尽管AOT生成的源代码可能较为冗长,但它相当易读,在调试应用程序时非常有帮助。 使用Maven时,生成的源文件位于target/spring-aot/main/sources目录下;使用Gradle时,则位于build/generated/aotSources目录下。

提示文件生成

除了生成源文件外,Spring AOT 引擎还会生成 GraalVM 使用的提示文件。 提示文件包含 JSON 数据,用于描述 GraalVM 应该如何处理那些无法通过直接检查代码来理解的内容。spring-doc.cadn.net.cn

例如,你可能会在一个私有方法上使用 Spring 注解。 Spring 需要使用反射来调用私有方法,即使在 GraalVM 上也是如此。 当出现此类情况时,Spring 可以写入一个反射提示(reflection hint),以便 GraalVM 知道:尽管该私有方法并未被直接调用,但在原生镜像中仍然需要保留该方法。spring-doc.cadn.net.cn

提示文件在 META-INF/native-image 目录下生成,GraalVM 会自动识别这些文件。spring-doc.cadn.net.cn

使用 Maven 时,生成的提示文件位于 target/spring-aot/main/resources;使用 Gradle 时,则位于 build/generated/aotResources

代理类生成

Spring 有时需要生成代理类,以向您编写的代码中添加额外功能。 为此,它使用 cglib 库直接生成字节码。spring-doc.cadn.net.cn

当应用程序在 JVM 上运行时,代理类会在应用程序运行过程中动态生成。 在创建原生镜像(native image)时,这些代理需要在构建时就生成,以便 GraalVM 能够将其包含进去。spring-doc.cadn.net.cn

与源代码生成不同,生成的字节码在调试应用程序时并不是特别有用。 然而,如果你需要使用诸如 .class 之类的工具检查 javap 文件的内容,可以在 Maven 项目的 target/spring-aot/main/classes 目录下,或 Gradle 项目的 build/generated/aotClasses 目录下找到它们。