提前优化

本章介绍 Spring 的预先优化 (AOT)。

有关特定于集成测试的 AOT 支持,请参阅 测试的预先优化支持

预先优化简介

Spring 对 AOT 优化的支持旨在检查构建时的 ApplicationContext,并应用通常在运行时发生的决策和发现逻辑。这样做可以构建一个应用程序启动安排,该安排更加直接,并且主要基于类路径和 Environment,专注于一组固定的功能。

早期应用此类优化意味着以下限制

  • 类路径在构建时是固定的且完全定义的。

  • 应用程序中定义的 bean 无法在运行时更改,这意味着

    • @Profile,特别是特定于配置文件的配置,需要在构建时选择,并在启用 AOT 时在运行时自动启用。

    • 影响 bean 存在的 Environment 属性(@Conditional)仅在构建时考虑。

  • 具有实例供应商(lambda 或方法引用)的 bean 定义无法预先转换。

  • 使用 registerSingleton(通常来自 ConfigurableListableBeanFactory)注册为单例的 Bean 也无法预先转换。

  • 由于我们无法依赖实例,因此请确保 Bean 类型尽可能精确。

另请参阅 最佳实践 部分。

当这些限制到位时,可以在构建时执行预先处理并生成其他资产。经过 Spring AOT 处理的应用程序通常会生成

  • Java 源代码

  • 字节码(通常用于动态代理)

  • RuntimeHints,用于反射、资源加载、序列化和 JDK 代理

目前,AOT 专注于允许使用 GraalVM 将 Spring 应用程序部署为本机映像。我们打算在未来版本中支持更多基于 JVM 的用例。

AOT 引擎概述

用于处理 ApplicationContext 的 AOT 引擎的入口点是 ApplicationContextAotGenerator。它根据表示要优化的应用程序的 GenericApplicationContextGenerationContext 来处理以下步骤

  • 刷新 ApplicationContext 以进行 AOT 处理。与传统刷新相反,此版本仅创建 Bean 定义,而不创建 Bean 实例。

  • 调用可用的 BeanFactoryInitializationAotProcessor 实现,并针对 GenerationContext 应用其贡献。例如,核心实现遍历所有候选 Bean 定义,并生成将 BeanFactory 状态还原为必要代码。

此过程完成后,GenerationContext 将使用应用程序运行所需的生成代码、资源和类进行更新。RuntimeHints 实例也可用于生成相关的 GraalVM 本机映像配置文件。

ApplicationContextAotGenerator#processAheadOfTime 返回 ApplicationContextInitializer 入口点的类名,该入口点允许使用 AOT 优化启动上下文。

以下部分将更详细地介绍这些步骤。

AOT 处理刷新

所有 GenericApplicationContext 实现都支持 AOT 处理刷新。应用程序上下文使用任意数量的入口点创建,通常采用 @Configuration 注释的类的形式。

我们来看一个基本示例

	@Configuration(proxyBeanMethods=false)
	@ComponentScan
	@Import({DataSourceConfiguration.class, ContainerConfiguration.class})
	public class MyApplication {
	}

使用常规运行时启动此应用程序涉及许多步骤,包括类路径扫描、配置类解析、Bean 实例化和生命周期回调处理。AOT 处理刷新仅应用 常规 refresh 中发生的部分操作。AOT 处理可以按如下方式触发

		RuntimeHints hints = new RuntimeHints();
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		context.register(MyApplication.class);
		context.refreshForAotProcessing(hints);
		// ...
		context.close();

在此模式中,BeanFactoryPostProcessor 实现将按常规调用。这包括配置类解析、导入选择器、类路径扫描等。此类步骤确保 BeanRegistry 包含应用程序的相关 Bean 定义。如果 Bean 定义受条件保护(例如 @Profile),则会对其进行评估,并且不符合其条件的 Bean 定义在此阶段将被丢弃。

如果自定义代码需要以编程方式注册额外的 Bean,请确保自定义注册代码使用 BeanDefinitionRegistry 而不是 BeanFactory,因为只有 Bean 定义才会被考虑。一个好的模式是实现 ImportBeanDefinitionRegistrar 并通过 @Import 在您的某个配置类中注册它。

由于此模式实际上不会创建 Bean 实例,因此不会调用 BeanPostProcessor 实现,但针对 AOT 处理相关的特定变体除外。这些是

  • MergedBeanDefinitionPostProcessor 实现对 Bean 定义进行后处理以提取其他设置,例如 initdestroy 方法。

  • SmartInstantiationAwareBeanPostProcessor 实现根据需要确定更精确的 Bean 类型。这确保创建运行时所需的任何代理。

此部分完成后,BeanFactory 将包含应用程序运行所需的 Bean 定义。它不会触发 Bean 实例化,但允许 AOT 引擎检查将在运行时创建的 Bean。

Bean Factory 初始化 AOT 贡献

想要参与此步骤的组件可以实现 BeanFactoryInitializationAotProcessor 接口。每个实现都可以基于 Bean Factory 的状态返回 AOT 贡献。

AOT 贡献是一个组件,它提供重现特定行为的生成代码。它还可以提供 RuntimeHints 以指示需要反射、资源加载、序列化或 JDK 代理。

BeanFactoryInitializationAotProcessor 实现可以在 META-INF/spring/aot.factories 中注册,其键等于该接口的全限定名称。

BeanFactoryInitializationAotProcessor 接口也可以由 Bean 直接实现。在此模式中,Bean 提供与它在常规运行时提供的特性等效的 AOT 贡献。因此,此类 Bean 会自动从经过 AOT 优化的上下文中排除。

如果某个 Bean 实现了 BeanFactoryInitializationAotProcessor 接口,那么该 Bean 和其所有依赖项都将在 AOT 处理期间进行初始化。我们通常建议仅由基础设施 Bean(例如 BeanFactoryPostProcessor)实现此接口,这些 Bean 具有有限的依赖项,并且已经在 Bean 工厂生命周期中较早地进行了初始化。如果使用 @Bean 工厂方法注册了此类 Bean,请确保该方法为 static,以便不必初始化其封闭的 @Configuration 类。

Bean 注册 AOT 贡献

一个核心 BeanFactoryInitializationAotProcessor 实现负责收集每个候选 BeanDefinition 的必要贡献。它使用专用的 BeanRegistrationAotProcessor 来执行此操作。

此接口的使用方式如下

  • BeanPostProcessor Bean 实现,以替换其运行时行为。例如,AutowiredAnnotationBeanPostProcessor 实现此接口以生成注入使用 @Autowired 注释的成员的代码。

  • 由在 META-INF/spring/aot.factories 中注册的类型实现,其键等于接口的全限定名称。通常在需要针对核心框架的特定功能调整 Bean 定义时使用。

如果某个 Bean 实现了 BeanRegistrationAotProcessor 接口,那么该 Bean 和其所有依赖项都将在 AOT 处理期间进行初始化。我们通常建议仅由基础设施 Bean(例如 BeanFactoryPostProcessor)实现此接口,这些 Bean 具有有限的依赖项,并且已经在 Bean 工厂生命周期中较早地进行了初始化。如果使用 @Bean 工厂方法注册了此类 Bean,请确保该方法为 static,以便不必初始化其封闭的 @Configuration 类。

如果没有 BeanRegistrationAotProcessor 处理某个特定已注册 Bean,则默认实现会处理它。这是默认行为,因为针对 Bean 定义调整生成的代码应仅限于特殊情况。

以我们之前的示例为例,我们假设 DataSourceConfiguration 如下所示

  • Java

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {

	@Bean
	public SimpleDataSource dataSource() {
		return new SimpleDataSource();
	}

}
@Configuration(proxyBeanMethods = false)
class DataSourceConfiguration {

	@Bean
	fun dataSource() = SimpleDataSource()

}
不支持使用无效 Java 标识符(不以字母开头、包含空格等)的带反引号的 Kotlin 类名称。

由于此类没有特定条件,因此 dataSourceConfigurationdataSource 被识别为候选。AOT 引擎将把上述配置类转换为类似于以下内容的代码

  • Java

/**
 * Bean definitions for {@link DataSourceConfiguration}
 */
@Generated
public class DataSourceConfiguration__BeanDefinitions {
	/**
	 * Get the bean definition for 'dataSourceConfiguration'
	 */
	public static BeanDefinition getDataSourceConfigurationBeanDefinition() {
		Class<?> beanType = DataSourceConfiguration.class;
		RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
		beanDefinition.setInstanceSupplier(DataSourceConfiguration::new);
		return beanDefinition;
	}

	/**
	 * Get the bean instance supplier for 'dataSource'.
	 */
	private static BeanInstanceSupplier<SimpleDataSource> getDataSourceInstanceSupplier() {
		return BeanInstanceSupplier.<SimpleDataSource>forFactoryMethod(DataSourceConfiguration.class, "dataSource")
				.withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(DataSourceConfiguration.class).dataSource());
	}

	/**
	 * Get the bean definition for 'dataSource'
	 */
	public static BeanDefinition getDataSourceBeanDefinition() {
		Class<?> beanType = SimpleDataSource.class;
		RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
		beanDefinition.setInstanceSupplier(getDataSourceInstanceSupplier());
		return beanDefinition;
	}
}
生成的代码可能因 bean 定义的确切性质而异。
每个生成的类都使用 org.springframework.aot.generate.Generated 进行注释,以标识它们是否需要排除,例如通过静态分析工具。

上面生成的代码创建的 bean 定义等同于 @Configuration 类,但直接且尽可能不使用反射。dataSourceConfiguration 有一个 bean 定义,dataSourceBean 也有一个。当需要 datasource 实例时,将调用 BeanInstanceSupplier。此供应商在 dataSourceConfiguration bean 上调用 dataSource() 方法。

使用 AOT 优化运行

AOT 是将 Spring 应用程序转换为本机可执行文件的强制步骤,因此在以这种模式运行时会自动启用它。可以通过将 spring.aot.enabled 系统属性设置为 true 来在 JVM 上使用这些优化。

当包含 AOT 优化时,在构建时做出的某些决策会硬编码到应用程序设置中。例如,在构建时启用的配置文件也会在运行时自动启用。

最佳实践

AOT 引擎旨在处理尽可能多的用例,而无需在应用程序中更改代码。但是,请记住,一些优化是在构建时基于 bean 的静态定义进行的。

本节列出了确保应用程序为 AOT 做好准备的最佳实践。

以编程方式注册 bean

AOT 引擎负责处理 @Configuration 模型和在处理配置时可能调用的任何回调。如果您需要以编程方式注册其他 bean,请务必使用 BeanDefinitionRegistry 来注册 bean 定义。

这通常可以通过 BeanDefinitionRegistryPostProcessor 来完成。请注意,如果它本身注册为 bean,则它将在运行时再次被调用,除非您确保也实现了 BeanFactoryInitializationAotProcessor。一种更惯用的方法是实现 ImportBeanDefinitionRegistrar 并使用 @Import 在您的某个配置类中注册它。这会将您的自定义代码作为配置类解析的一部分进行调用。

如果您使用不同的回调以编程方式声明其他 bean,则 AOT 引擎可能不会处理它们,因此不会为它们生成提示。根据环境的不同,这些 bean 可能根本不会注册。例如,类路径扫描在原生映像中不起作用,因为没有类路径的概念。对于这种情况,至关重要的是在构建时进行扫描。

公开最精确的 bean 类型

虽然您的应用程序可能与 bean 实现的接口进行交互,但声明最精确的类型仍然非常重要。AOT 引擎对 bean 类型执行其他检查,例如检测是否存在 @Autowired 成员或生命周期回调方法。

对于 @Configuration 类,确保工厂 @Bean 方法的返回类型尽可能精确。考虑以下示例

  • Java

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

	@Bean
	public MyInterface myInterface() {
		return new MyImplementation();
	}

}

在上面的示例中,myInterface bean 的声明类型为 MyInterface。通常的后处理都不会考虑 MyImplementation。例如,如果 MyImplementation 上有上下文应该注册的带注释的处理程序方法,则不会预先检测到它。

上面的示例应重写如下

  • Java

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

	@Bean
	public MyImplementation myInterface() {
		return new MyImplementation();
	}

}

如果您以编程方式注册 bean 定义,请考虑使用 RootBeanBefinition,因为它允许指定处理泛型的 ResolvableType

避免使用多个构造函数

容器能够根据多个候选构造函数选择最合适的构造函数。但是,这不是最佳实践,最好在必要时使用 @Autowired 标记首选构造函数。

如果您正在处理无法修改的代码库,则可以在相关 bean 定义上设置 preferredConstructors 属性 以指示应使用哪个构造函数。

避免使用自定义参数创建 Bean

Spring AOT 检测创建 bean 所需的操作,并使用实例供应商将其转换为生成代码中。容器还支持使用 自定义参数 创建 bean,这会导致 AOT 出现多个问题

  1. 自定义参数需要对匹配的构造函数或工厂方法进行动态内省。AOT 无法检测到这些参数,因此必须手动提供必要的反射提示。

  2. 绕过实例供应商意味着创建后所有其他优化也会被跳过。例如,将跳过字段和方法上的自动装配,因为它们在实例供应商中得到处理。

与其使用自定义参数创建原型作用域的 bean,我们建议使用手动工厂模式,其中一个 bean 负责创建实例。

FactoryBean

FactoryBean 应谨慎使用,因为它在 bean 类型解析方面引入了一个中间层,在概念上可能没有必要。根据经验,如果 FactoryBean 实例不保存长期状态并且在运行时以后不需要,则应将其替换为常规工厂方法,可能在顶部有一个 FactoryBean 适配器层(用于声明式配置目的)。

如果您的 FactoryBean 实现未解析对象类型(即 T),则需要格外小心。考虑以下示例

  • Java

public class ClientFactoryBean<T extends AbstractClient> implements FactoryBean<T> {
	// ...
}

具体客户端声明应为客户端提供已解析的泛型,如下例所示

  • Java

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

	@Bean
	public ClientFactoryBean<MyClient> myClient() {
		return new ClientFactoryBean<>(...);
	}

}

如果 FactoryBean bean 定义以编程方式注册,请务必执行以下步骤

  1. 使用 RootBeanDefinition

  2. beanClass 设置为 FactoryBean 类,以便 AOT 知道它是一个中间层。

  3. ResolvableType 设置为已解析的泛型,以确保公开最精确的类型。

以下示例展示了一个基本定义

  • Java

RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class);
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class));
// ...
registry.registerBeanDefinition("myClient", beanDefinition);

JPA

JPA 持久性单元必须预先了解,以便应用某些优化。考虑以下基本示例

  • Java

@Bean
LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource) {
	LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
	factoryBean.setDataSource(dataSource);
	factoryBean.setPackagesToScan("com.example.app");
	return factoryBean;
}

为了确保扫描提前进行,必须声明 PersistenceManagedTypes bean 并由工厂 bean 定义使用,如下例所示

  • Java

@Bean
PersistenceManagedTypes persistenceManagedTypes(ResourceLoader resourceLoader) {
	return new PersistenceManagedTypesScanner(resourceLoader)
			.scan("com.example.app");
}

@Bean
LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource, PersistenceManagedTypes managedTypes) {
	LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
	factoryBean.setDataSource(dataSource);
	factoryBean.setManagedTypes(managedTypes);
	return factoryBean;
}

运行时提示

与常规 JVM 运行时相比,将应用程序作为本机映像运行需要更多信息。例如,GraalVM 需要预先知道组件是否使用反射。同样,除非明确指定,否则类路径资源不会包含在本机映像中。因此,如果应用程序需要加载资源,则必须从相应的 GraalVM 本机映像配置文件中引用该资源。

RuntimeHints API 在运行时收集对反射、资源加载、序列化和 JDK 代理的需求。以下示例确保可以在本机映像中从类路径加载 config/app.properties

  • Java

runtimeHints.resources().registerPattern("config/app.properties");

在 AOT 处理期间自动处理许多契约。例如,会检查 @Controller 方法的返回类型,如果 Spring 检测到该类型应该被序列化(通常为 JSON),则会添加相关的反射提示。

对于核心容器无法推断的情况,您可以以编程方式注册此类提示。还为常见用例提供了一些便捷的注释。

@ImportRuntimeHints

RuntimeHintsRegistrar 实现允许你获取 AOT 引擎管理的 RuntimeHints 实例的回调。此接口的实现可以使用 @ImportRuntimeHints 在任何 Spring bean 或 @Bean 工厂方法上注册。RuntimeHintsRegistrar 实现会在构建时检测并调用。

import java.util.Locale;

import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

@Component
@ImportRuntimeHints(SpellCheckService.SpellCheckServiceRuntimeHints.class)
public class SpellCheckService {

	public void loadDictionary(Locale locale) {
		ClassPathResource resource = new ClassPathResource("dicts/" + locale.getLanguage() + ".txt");
		//...
	}

	static class SpellCheckServiceRuntimeHints implements RuntimeHintsRegistrar {

		@Override
		public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
			hints.resources().registerPattern("dicts/*");
		}
	}

}

如果可能,应尽可能将 @ImportRuntimeHints 用于需要提示的组件。这样,如果组件未添加到 BeanFactory,提示也不会添加。

还可以通过在 META-INF/spring/aot.factories 中添加一个条目来静态注册实现,其键等于 RuntimeHintsRegistrar 接口的全限定名。

@Reflective

@Reflective 提供了一种惯用的方式来标记对注释元素进行反射的需要。例如,@EventListener 使用 @Reflective 进行元注释,因为底层实现使用反射调用注释方法。

默认情况下,只考虑 Spring bean,并为注释元素注册调用提示。可以通过 @Reflective 注释指定自定义 ReflectiveProcessor 实现来调整此设置。

库作者可以将此注释重新用于自己的目的。如果需要处理除 Spring bean 之外的组件,BeanFactoryInitializationAotProcessor 可以检测相关类型并使用 ReflectiveRuntimeHintsRegistrar 来处理它们。

@RegisterReflectionForBinding

@RegisterReflectionForBinding@Reflective 的一种特殊化,用于注册序列化任意类型的需要。典型用例是使用容器无法推断的 DTO,例如在方法体中使用 Web 客户端。

@RegisterReflectionForBinding 可以应用于类级别的任何 Spring bean,但也可以直接应用于方法、字段或构造函数,以更好地指示实际需要提示的位置。以下示例注册了 Account 以进行序列化。

  • Java

@Component
public class OrderService {

	@RegisterReflectionForBinding(Account.class)
	public void process(Order order) {
		// ...
	}

}

测试运行时提示

Spring Core 还提供 RuntimeHintsPredicates,一种用于检查现有提示是否与特定用例匹配的实用工具。这可以在您自己的测试中用于验证 RuntimeHintsRegistrar 是否包含预期结果。我们可以为我们的 SpellCheckService 编写一个测试,并确保我们能够在运行时加载词典

	@Test
	void shouldRegisterResourceHints() {
		RuntimeHints hints = new RuntimeHints();
		new SpellCheckServiceRuntimeHints().registerHints(hints, getClass().getClassLoader());
		assertThat(RuntimeHintsPredicates.resource().forResource("dicts/en.txt"))
				.accepts(hints);
	}

使用 RuntimeHintsPredicates,我们可以检查反射、资源、序列化或代理生成提示。这种方法非常适合单元测试,但暗示组件的运行时行为是众所周知的。

您可以通过使用 GraalVM 跟踪代理 运行其测试套件(或应用程序本身)来了解应用程序的全局运行时行为。此代理将记录在运行时需要 GraalVM 提示的所有相关调用,并将它们写出为 JSON 配置文件。

为了进行更有针对性的发现和测试,Spring Framework 提供了一个包含核心 AOT 测试实用程序的专用模块 "org.springframework:spring-core-test"。此模块包含 RuntimeHints Agent,这是一个 Java 代理,用于记录与运行时提示相关的所有方法调用,并帮助您断言给定的 RuntimeHints 实例涵盖所有记录的调用。让我们考虑一个基础架构,我们希望测试我们在 AOT 处理阶段提供的提示。

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.util.ClassUtils;

public class SampleReflection {

	private final Log logger = LogFactory.getLog(SampleReflection.class);

	public void performReflection() {
		try {
			Class<?> springVersion = ClassUtils.forName("org.springframework.core.SpringVersion", null);
			Method getVersion = ClassUtils.getMethod(springVersion, "getVersion");
			String version = (String) getVersion.invoke(null);
			logger.info("Spring version:" + version);
		}
		catch (Exception exc) {
			logger.error("reflection failed", exc);
		}
	}

}

然后,我们可以编写一个单元测试(不需要本机编译)来检查我们提供的提示

import java.util.List;

import org.junit.jupiter.api.Test;

import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent;
import org.springframework.aot.test.agent.RuntimeHintsInvocations;
import org.springframework.aot.test.agent.RuntimeHintsRecorder;
import org.springframework.core.SpringVersion;

import static org.assertj.core.api.Assertions.assertThat;

// @EnabledIfRuntimeHintsAgent signals that the annotated test class or test
// method is only enabled if the RuntimeHintsAgent is loaded on the current JVM.
// It also tags tests with the "RuntimeHints" JUnit tag.
@EnabledIfRuntimeHintsAgent
class SampleReflectionRuntimeHintsTests {

	@Test
	void shouldRegisterReflectionHints() {
		RuntimeHints runtimeHints = new RuntimeHints();
		// Call a RuntimeHintsRegistrar that contributes hints like:
		runtimeHints.reflection().registerType(SpringVersion.class, typeHint ->
				typeHint.withMethod("getVersion", List.of(), ExecutableMode.INVOKE));

		// Invoke the relevant piece of code we want to test within a recording lambda
		RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
			SampleReflection sample = new SampleReflection();
			sample.performReflection();
		});
		// assert that the recorded invocations are covered by the contributed hints
		assertThat(invocations).match(runtimeHints);
	}

}

如果您忘记提供提示,测试将失败并提供有关调用的某些详细信息

org.springframework.docs.core.aot.hints.testing.SampleReflection performReflection
INFO: Spring version:6.0.0-SNAPSHOT

Missing <"ReflectionHints"> for invocation <java.lang.Class#forName>
with arguments ["org.springframework.core.SpringVersion",
    false,
    jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7].
Stacktrace:
<"org.springframework.util.ClassUtils#forName, Line 284
io.spring.runtimehintstesting.SampleReflection#performReflection, Line 19
io.spring.runtimehintstesting.SampleReflectionRuntimeHintsTests#lambda$shouldRegisterReflectionHints$0, Line 25

有各种方法可以在您的构建中配置此 Java 代理,因此请参阅构建工具和测试执行插件的文档。代理本身可以配置为检测特定包(默认情况下,只检测 org.springframework)。您可以在 Spring Framework buildSrc 自述文件 中找到更多详细信息。