提前期优化

本章涵盖了 Spring 的提前期 (AOT) 优化。 有关集成测试的 AOT 支持,请参阅 测试的提前期支持

提前期优化简介

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

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

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

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

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

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

  • 带有实例供应商(lambda 或方法引用)的 bean 定义无法提前转换。

  • 注册为单例的 bean(使用 registerSingleton,通常来自 ConfigurableListableBeanFactory)也无法提前转换。

  • 由于我们不能依赖实例,请确保 bean 类型尽可能精确。

另请参阅 最佳实践 部分。

当这些限制到位时,就可以在构建时执行提前处理并生成额外的资产。 Spring AOT 处理的应用程序通常会生成:

  • Java 源代码

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

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

目前,AOT 专注于允许 Spring 应用程序使用 GraalVM 作为原生镜像部署。 我们打算在未来支持更多基于 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 注解的类。

我们来看一个基本示例:

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

在此模式下,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 工厂的状态返回 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 工厂方法注册,请确保该方法是 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 工厂方法注册,请确保该方法是 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 注解,以便 在需要将其排除时识别它们,例如通过静态分析工具。

上面生成的代码创建了等效于 @Configuration 类的 bean 定义,但以直接的方式,并且如果可能的话,完全不使用反射。 dataSourceConfigurationdataSourceBean 各有一个 bean 定义。 当需要 datasource 实例时,会调用 BeanInstanceSupplier。 此供应商会在 dataSourceConfiguration bean 上调用 dataSource() 方法。

使用 AOT 优化运行

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

当包含 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

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

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

}
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

	@Bean
	fun myInterface(): MyInterface = MyImplementation()

}

在上面的示例中,myInterface bean 的声明类型是 MyInterface。 在 AOT 处理期间,所有常规的后处理都不会考虑 MyImplementation。 例如,如果在 MyImplementation 上有一个带注解的处理程序方法,并且上下文应该注册它,那么在 AOT 处理期间将不会检测到它。

因此,上面的示例应该重写如下:

  • Java

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

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

}
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

	@Bean
	fun myInterface() = MyImplementation()

}

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

避免多个构造函数

容器能够根据多个候选者选择最合适的构造函数。 然而,依赖这种方式并不是最佳实践,如果需要,最好使用 @Autowired 标记首选构造函数。

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

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

以编程方式创建 RootBeanDefinition 时,您在可使用的类型方面不受限制。 例如,您可能有一个自定义 record,其中包含您的 bean 作为构造函数参数的几个属性。

虽然这在常规运行时工作正常,但 AOT 不知道如何生成自定义数据结构的代码。 一个好的经验法则是记住 bean 定义是多种模型之上的抽象。 建议不要使用此类结构,而是分解为简单类型或引用以此方式构建的 bean。

作为最后的手段,您可以实现自己的 org.springframework.aot.generate.ValueCodeGenerator$Delegate。 要使用它,请在 META-INF/spring/aot.factories 中注册其完全限定名,并将 org.springframework.aot.generate.ValueCodeGenerator$Delegate 作为键。

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

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

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

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

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

避免循环依赖

某些用例可能导致一个或多个 bean 之间存在循环依赖。在 常规运行时,可以通过 @Autowired 在 setter 方法或字段上 连接这些循环依赖。但是,AOT 优化的上下文将无法启动 明确的循环依赖。

因此,在 AOT 优化的应用程序中,您应该努力避免循环 依赖。如果无法避免,您可以使用 @Lazy 注入点或 ObjectProvider 来延迟访问或检索必要的协作 bean。请参阅 此提示 以获取更多信息。

FactoryBean

FactoryBean 应谨慎使用,因为它在 bean 类型解析方面引入了一个中间层,这在概念上可能不是必需的。 一般而言,如果 FactoryBean 实例不持有长期状态,并且在运行时后期不需要,则应将其替换为常规的 @Bean 工厂方法,可能在其之上添加 FactoryBean 适配器层(用于声明式配置目的)。

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

  • Java

  • Kotlin

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

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

  • Java

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

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

}
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

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

}

如果 FactoryBean bean 定义以编程方式注册,请确保遵循以下步骤:

  1. 使用 RootBeanDefinition

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

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

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

  • Java

  • Kotlin

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

JPA

JPA 持久性单元必须提前知道才能应用某些优化。考虑以下基本示例:

  • Java

  • Kotlin

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

为了确保实体扫描提前发生,必须声明一个 PersistenceManagedTypes bean 并由工厂 bean 定义使用,如以下示例所示:

  • Java

  • Kotlin

@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;
}
@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
}

运行时提示

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

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

  • Java

  • Kotlin

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

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

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

@ImportRuntimeHints

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

如果可能,@ImportRuntimeHints 应尽可能靠近需要提示的组件使用。 这样,如果组件未贡献给 BeanFactory,则提示也不会贡献。

也可以通过在 META-INF/spring/aot.factories 中添加一个条目并以 RuntimeHintsRegistrar 接口的完全限定名作为键来静态注册实现。

@Reflective

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

开箱即用,只考虑 Spring bean,但您可以使用 @ReflectiveScan 选择性地进行扫描。在 下面的示例中,com.example.app 包及其子包中的所有类型都被考虑:

扫描发生在 AOT 处理期间,目标包中的类型不需要类级别注解即可被考虑。 这会执行 深度扫描,并检查类型、字段、构造函数、方法和封闭元素上是否存在 @Reflective,无论是直接存在还是作为元注解存在。

默认情况下,@Reflective 为注解元素注册一个调用提示。 这可以通过 @Reflective 注解指定自定义 ReflectiveProcessor 实现来调整。

库作者可以出于自己的目的重用此注解。 此类自定义的示例将在下一节中介绍。

@RegisterReflection

@RegisterReflection@Reflective 的一种特殊形式,它提供了一种声明性方式来为任意类型注册反射。

作为 @Reflective 的特化,如果您正在使用 @ReflectiveScan@RegisterReflection 也会被检测到。

在以下示例中,AccountService 上的公共构造函数和公共方法可以通过反射调用:

@RegisterReflection 可以应用于类级别的任何目标类型,但也可以直接应用于方法以更好地指示实际需要提示的位置。

@RegisterReflection 可以用作元注解以支持更具体的需求。 @RegisterReflectionForBinding 是一个复合注解,它使用 @RegisterReflection 进行元注解,并注册对任意类型进行序列化的需求。 一个典型的用例是在方法体中使用 web 客户端时容器无法推断的 DTO。

以下示例注册 Order 以进行序列化。

这会为 Order 的构造函数、字段、属性和记录组件注册提示。 还会为属性和记录组件上间接使用的类型注册提示。 换句话说,如果 Order 公开其他类型,也会为这些类型注册提示。

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

尽管核心容器提供了对许多常见类型自动转换的内置支持 (请参阅 Spring 类型转换),但某些转换是通过 依赖反射的基于约定的算法来支持的。

具体来说,如果 ConversionService 中没有为特定的源 → 目标类型对注册显式 Converter,则内部的 ObjectToObjectConverter 将尝试使用约定将源对象转换为目标类型, 方法是委托给源对象上的方法或目标类型上的静态工厂方法或构造函数。 由于此基于约定的算法可以在运行时应用于任意类型,因此核心容器无法推断支持此类反射所需的运行时提示。

如果您遇到原生镜像中由于缺少运行时提示而导致的基于约定的转换问题, 您可以以编程方式注册必要的提示。例如,如果您的应用程序需要将 java.time.Instant 转换为 java.sql.Timestamp,并依赖 ObjectToObjectConverter 使用反射调用 java.sql.Timestamp.from(Instant),您可以实现自定义的 RuntimeHintsRegitrar 来支持原生镜像中的此用例, 如以下示例所示。

  • 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")));
	}
}

然后,TimestampConversionRuntimeHints 可以通过 @ImportRuntimeHints 声明性地注册,或者通过 META-INF/spring/aot.factories 配置文件静态注册。

上述 TimestampConversionRuntimeHints 类是框架中包含并默认注册的 ObjectToObjectConverterRuntimeHints 类的简化版本。 因此,这个特定的 Instant-to-Timestamp 用例已经由框架处理。

测试运行时提示

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

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

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

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

然后我们可以编写一个单元测试(无需原生编译)来检查我们贡献的提示:

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

org.springframework.docs.core.aot.hints.testing.SampleReflection performReflection
INFO: Spring version: 6.2.0

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 README 文件中找到更多详细信息。