Skip to content

提前编译优化 (Ahead of Time Optimizations)

本章涵盖了 Spring 的提前编译 (AOT) 优化技术。

有关针对集成测试的 AOT 支持,请参阅测试的提前编译支持

提前编译优化简介

Spring 对 AOT 优化的支持旨在在构建阶段检查 ApplicationContext,并应用通常在运行时发生的决策和探测逻辑。这样做可以构建一个更直接、更专注于一组固定功能的应用程序启动方案,这些功能主要基于类路径(classpath)和环境(Environment)。

提前应用此类优化意味着存在以下限制

  • 类路径固定:类路径在构建阶段是固定且完整定义的。
  • Bean 定义不可变:应用程序中定义的 Bean 无法在运行时更改,这意味着:
    • @Profile(特别是特定 Profile 的配置)需要在构建阶段选定,并在启用 AOT 时在运行时自动启用。
    • 影响 Bean 存在的 Environment 属性(@Conditional)仅在构建阶段考虑。
  • 不支持实例供应者:使用实例供应者(Lambda 表达式或方法引用)的 Bean 定义无法提前转换。
  • 不支持单例注册:作为单例注册的 Bean(通常使用 ConfigurableListableBeanFactoryregisterSingleton)也无法提前转换。
  • 确保类型精确:由于我们无法依赖运行时实例,请确保 Bean 类型尽可能精确。

提示

另请参阅最佳实践章节。

在这些限制下,可以在构建阶段执行提前处理并生成额外的资产。一个经过 Spring AOT 处理的应用程序通常会生成:

  • Java 源代码
  • 字节码(通常用于动态代理)。
  • 运行提示 (RuntimeHints):用于反射、资源加载、序列化和 JDK 代理。

注意

目前,AOT 的重点是允许 Spring 应用程序使用 GraalVM 部署为原生镜像 (Native Image)。我们打算在未来的版本中支持更多基于 JVM 的用例。

AOT 引擎概述

AOT 引擎处理 ApplicationContext 的入口点是 ApplicationContextAotGenerator。它基于代表待优化应用的 GenericApplicationContextGenerationContext,负责以下步骤:

  • 为 AOT 处理刷新 ApplicationContext:与传统的刷新不同,此版本仅创建 Bean 定义,而不创建 Bean 实例。
  • 调用 AOT 处理器:调用可用的 BeanFactoryInitializationAotProcessor 实现,并将其贡献应用到 GenerationContext 中。例如,核心实现会迭代所有候选 Bean 定义,并生成还原 BeanFactory 状态所需的必要代码。

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

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

为 AOT 处理进行刷新

所有 GenericApplicationContext 实现都支持为 AOT 处理进行刷新。应用程序上下文是通过任意数量的入口点(通常是标注了 @Configuration 的类)创建的。

让我们看一个基础示例:

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

在常规运行时启动此应用涉及类路径扫描、配置类解析、Bean 实例化和生命周期回调处理。而 AOT 处理刷新仅应用常规 refresh 的一个子集。

触发 AOT 处理的方式如下:

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

在此模式下,BeanFactoryPostProcessor 实现会照常被调用。这包括配置类解析、导入选择器、类路径扫描等。这些步骤确保 BeanRegistry 包含应用所需的所有 Bean 定义。如果有被条件(如 @Profile)守护的 Bean 定义,它们会被评估,不满足条件的定义将在该阶段被丢弃。

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

由于此模式不实际创建 Bean 实例,因此不会调用 BeanPostProcessor,但以下与 AOT 处理相关的特定变体除外:

  • MergedBeanDefinitionPostProcessor 实现:后处理 Bean 定义以提取初始化和销毁方法等设置。
  • SmartInstantiationAwareBeanPostProcessor 实现:如果需要,确定更精确的 Bean 类型。这确保了创建运行时所需的任何代理。

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

Bean Factory 初始化 AOT 贡献

想要参与此步骤的组件可以实现 BeanFactoryInitializationAotProcessor 接口。每个实现可以根据 Bean Factory 的状态返回一个 AOT 贡献(Contribution)。

AOT 贡献是一个能够产生生成代码的组件,这些代码用于复现特定的行为。它还可以贡献 RuntimeHints,以指示对反射、资源加载、序列化或 JDK 代理的需求。

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

该接口也可以直接由 Bean 实现。在此模式下,Bean 提供的 AOT 贡献等价于它在常规运行时提供的功能。因此,此类 Bean 会被自动排除在 AOT 优化的上下文之外。

注意

如果一个 Bean 实现了 BeanFactoryInitializationAotProcessor 接口,该 Bean 及其所有依赖项将在 AOT 处理期间被初始化。我们通常建议仅由基础设施 Bean 实现此接口(例如 BeanFactoryPostProcessor),因为这些 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,请确保它是 static 的。

如果没有 BeanRegistrationAotProcessor 处理特定的注册 Bean,将使用默认实现处理它。这是默认行为,因为对 Bean 定义生成的代码进行调优应仅限于特殊情况。

延续之前的示例,假设 DataSourceConfiguration 如下:

java
@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {

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

}
kotlin
@Configuration(proxyBeanMethods = false)
class DataSourceConfiguration {

	@Bean
	fun dataSource() = SimpleDataSource()

}

警告

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

由于此类没有任何特定条件,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,以便在需要排除它们时(例如通过静态分析工具)进行识别。

上述代码创建了与 @Configuration 类等效的 Bean 定义,但尽可能采用直接方式且不使用反射。当需要 datasource 实例时,会调用一个 BeanInstanceSupplier。该供应商会调用 dataSourceConfiguration Bean 上的 dataSource() 方法。

运行 AOT 优化

AOT 是将 Spring 应用程序转换为原生可执行文件的必要步骤,因此在原生镜像中运行时会自动启用。此外,也可以通过将 spring.aot.enabled 系统属性设置为 true 在 JVM 上使用 AOT 优化。

注意

启用 AOT 优化后,一些在构建时做出的决定会被硬编码到应用程序配置中。例如,在构建时启用的 Profile 也会在运行时自动启用。

最佳实践

AOT 引擎旨在处理尽可能多的用例,且无需更改应用程序代码。但是,请注意某些优化是在构建时基于 Bean 的静态定义进行的。

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

编程式 Bean 注册

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

这通常可以通过 BeanDefinitionRegistryPostProcessor 实现。请注意,如果它本身作为 Bean 注册,除非您也实现了 BeanFactoryInitializationAotProcessor,否则它将在运行时被再次调用。更惯用的方式是实现 ImportBeanDefinitionRegistrar 并通过 @Import 注册它。

如果使用不同的回调以编程方式声明额外的 Bean,它们很可能不会被 AOT 引擎处理,因此不会生成任何提示。在原生镜像中,类路径扫描不起作用,因此扫描必须在构建阶段执行。

暴露最精确的 Bean 类型

虽然应用可能与接口交互,但声明最精确的类型仍然至关重要。AOT 引擎会对 Bean 类型执行额外检查(如探测 @Autowired 成员或生命周期回调)。

对于 @Configuration 类,请确保 @Bean 工厂方法的返回类型尽可能精确:

java
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

	@Bean
	public MyImplementation myInterface() { // 尽量不要返回接口 MyInterface
		return new MyImplementation();
	}

}
kotlin
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

	@Bean
	fun myInterface() = MyImplementation()

}

如果返回的是接口,AOT 处理期间将无法探测到实现类上的特定标注。如果你是编程式注册,请考虑使用 RootBeanDefinition,因为它允许指定处理泛型的 ResolvableType

避免使用多个构造函数

容器能根据候选者选择最合适的构造函数,但依赖此动态选择并非最佳实践。建议在必要时使用 @Autowired 标记首选构造函数。

如果你无法修改代码库,可以在相关的 Bean 定义上设置 preferredConstructors 属性来指示构造函数。

避免在构造函数参数和属性中使用复杂数据结构

编程式构建 RootBeanDefinition 时,类型没有限制,例如可以使用自定义 record。虽然常规运行时没问题,但 AOT 无法自动生成此类自定义结构的还原代码。

经验法则是:保持 Bean 定义抽象为简单类型,或引用另一个以这种方式构建的 Bean。作为最后手段,你可以实现自己的 org.springframework.aot.generate.ValueCodeGenerator$Delegate 并在 aot.factories 中注册。

避免创建带有自定义参数的 Bean

Spring AOT 会探测创建 Bean 所需的操作。容器支持通过 自定义参数 创建 Bean,但这在 AOT 中会产生问题:

  1. 需要动态内省匹配的构造函数,且这些参数无法被 AOT 探测,必须手动提供反射提示。
  2. 绕过 instanceSupplier 意味着后续所有优化(如自动注入)都会被跳过。

推荐采用手动工厂模式(一个 Bean 负责创建实例),而不是使用自定义参数创建原型作用域的 Bean。

避免循环依赖

虽然常规运行可以通过 Setter 或字段注入解决循环依赖,但 AOT 优化的上下文在显式循环依赖下会启动失败。

应尽量避免循环依赖。如果无法避免,请使用 @Lazy 注入点或 ObjectProvider 实现延迟访问。

FactoryBean

应谨慎使用 FactoryBean,因为它引入了 Bean 类型解析的中介层。如果 FactoryBean 不保存长期状态,建议将其替换为常规的 @Bean 工厂方法。

如果你的 FactoryBean 无法完全解析对象类型(即泛型 T),需要格外小心。建议显式提供解析后的泛型:

java
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

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

}
kotlin
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

	@Bean
	fun myClient() = ClientFactoryBean<MyClient>(...)

}

如果是编程式注册:

  1. 使用 RootBeanDefinition
  2. beanClass 设置为 FactoryBean 类型以便 AOT 识别中介。
  3. 设置 ResolvableType 为已解析的泛型。

JPA

为了让一些优化生效,必须预先知道 JPA 持久化单元。为确保实体扫描提前进行,必须声明一个 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;
}
kotlin
@Bean
fun persistenceManagedTypes(resourceLoader: ResourceLoader): PersistenceManagedTypes {
	return PersistenceManagedTypesScanner(resourceLoader)
			.scan("com.example.app")
}

@Bean
fun customDBEntityManagerFactory(dataSource: DataSource, managedTypes: PersistenceManagedTypes): LocalContainerEntityManagerFactoryBean {
	val factoryBean = LocalContainerEntityManagerFactoryBean()
	factoryBean.dataSource = dataSource
	factoryBean.setManagedTypes(managedTypes)
	return factoryBean
}

运行时提示 (Runtime Hints)

以原生镜像运行应用需要额外信息,例如预先知道哪些组件使用反射、加载哪些资源。RuntimeHints API 负责在构建时收集这些需求。

以下是一个注册类路径资源的示例:

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

AOT 处理期间会自动处理许多条约。例如,如果 Controller 返回类型需要序列化为 JSON,Spring 会自动添加反射提示。对于无法推断的情况,需要手动注册或使用提供的便捷注解。

@ImportRuntimeHints

RuntimeHintsRegistrar 接口允许你获得 RuntimeHints 实例的回调。你可以通过在 Bean 或工厂方法上使用 @ImportRuntimeHints 来注册实现。这些实现会在构建时被探测并调用。

java
@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

@Reflective

@Reflective 提供了一种惯用的方式来标记对反射的需求(如 @EventListener 内部就集成了该注解)。

默认仅考虑 Spring Bean,但你可以使用 @ReflectiveScan 开启包扫描。扫描发生在 AOT 处理期间,执行“深层扫描”以检查类型、字段、方法等是否直接或通过元注解标注了 @Reflective

@RegisterReflection

@RegisterReflection@Reflective 的一种特殊形式,允许声明式地注册任意类型的反射提示:

java
@Configuration
@RegisterReflection(classes = AccountService.class, memberCategories =
		{ MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS })
class MyConfiguration {
}

此外,@RegisterReflectionForBinding 是一个组合注解,用于注册序列化任意类型的需求(典型场景是在方法体中使用 Web 客户端处理 DTO)。

基于约定的转换的运行时提示

Spring 的 ObjectToObjectConverter 有时会利用反射和约定(如静态 from 方法)来进行转换。由于这些是运行时动态发生的,核心容器无法推断它们,因此可能需要手动注册提示。

java
public class TimestampConversionRuntimeHints implements RuntimeHintsRegistrar {

	public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
		ReflectionHints reflectionHints = hints.reflection();

		reflectionHints.registerTypeIfPresent(classLoader, "java.sql.Timestamp", hint -> hint
				.withMethod("from", List.of(TypeReference.of(Instant.class)), ExecutableMode.INVOKE)
				.onReachableType(TypeReference.of("java.sql.Timestamp")));
	}
}

注意

Spring 框架默认已经包含了 ObjectToObjectConverterRuntimeHints 以处理常见的转换(如 InstantTimestamp),开发者仅需针对自己的业务转换关注此项。

测试运行时提示

Spring Core 提供了 RuntimeHintsPredicates 工用具用于验证提示是否匹配预期:

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

对于更复杂的场景,可以使用 GraalVM 跟踪代理 (Tracing Agent),或者使用 spring-core-test 模块中的 RuntimeHints Agent。它会记录运行时的实际调用并与你的 RuntimeHints 进行断言匹配,如果遗漏了提示,测试将失败并显示详细堆栈。


补充教学

1. AOT 的核心价值:云原生时代的“入场券”

在传统的 JVM 模式下,Spring 在启动时要做大量的扫描和动态推论,这被称为“热启动”。虽然对于服务器应用很强大,但在 Serverless 或微服务扩容场景下,缓慢的启动速度和庞大的内存占用(JIT 编译器和元数据的开销)是短板。

AOT (Ahead of Time) 技术将原本在运行时进行的探测、解析工作提前到了构建编译阶段。配合 GraalVM,它可以将应用编译成机器码,实现:

  • 极致启动速度:从秒级降至毫秒级。
  • 轻量级内存:不再需要庞大的 JIT 基础框架。

2. 类路径扫描的“消失”

在 AOT 模式下,传统的“动态类路径扫描”不见了。Spring 会在构建时生成代码来显式注册所有 Bean。这意味着:

  • 你不能通过在运行时动态向 plugins/ 目录添加 Jar 包来让 @ComponentScan 探测。
  • 应用的行为在编译后基本是“静态化”和“不可变”的。

3. 反射:从“理所当然”到“提前申报”

在 Java 开发中,反射无处不在。但在 AOT 特别是原生镜像中,反射默认是被禁用的,因为编译器无法提前优化不可知的调用。这就是 RuntimeHints 存在的意义。

  • 建议:尽量让 Spring 容器管理你的 Bean。如果是你自己写的工具类使用了深度反射,请务必使用 @Reflective 或其变体进行“申报”。

4. SpEL 的动态性与限制

SpEL 表达式异常灵活,但这种灵活性在 AOT 面前是个挑战。AOT 引擎会尝试解析 @Value 等位置的表达式,但如果表达式过于复杂(例如基于极其复杂的逻辑引用运行时的属性),可能会解析失败。

  • 准则:在追求 Native Image 兼容性时,保持表达式简单且可预测。

Based on Spring Framework.