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(通常使用 ConfigurableListableBeanFactory 的 registerSingleton)也无法提前转换。
  • 确保类型精确:由于我们无法依赖运行时实例,请确保 Bean 类型尽可能精确。

提示

另请参阅最佳实践章节。

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

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

注意

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

AOT 引擎概述 ​

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

  • 为 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 标识符(如不以字母开头、包含空格等)的类名是不支持的。

由于此类没有任何特定条件,dataSourceConfiguration 和 dataSource 被识别为候选对象。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 以处理常见的转换(如 Instant 到 Timestamp),开发者仅需针对自己的业务转换关注此项。

测试运行时提示 ​

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.