提前优化

本章涵盖了 Spring 的提前 (AOT) 优化。

有关特定于集成测试的 AOT 支持,请参阅测试的提前支持

提前优化简介

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

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

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

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

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

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

  • 具有实例提供程序(lambda 表达式或方法引用)的 Bean 定义无法提前转换。

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

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

另请参阅最佳实践部分。

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

  • Java 源代码

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

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

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

AOT 引擎概述

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

  • 刷新用于 AOT 处理的ApplicationContext。与传统的刷新相反,此版本仅创建 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 工厂初始化 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 工厂方法注册此类 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 注解,以便在需要排除它们时(例如,通过静态分析工具)识别它们。

上面生成的代码创建了等效于 @Configuration 类的 Bean 定义,但以直接方式且尽可能不使用反射。有一个 dataSourceConfiguration 的 Bean 定义和一个 dataSourceBean 的 Bean 定义。当需要 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

  • 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。通常的后处理都不会考虑 MyImplementation。例如,如果 MyImplementation 上有一个上下文应注册的带注释的处理程序方法,则它不会被预先检测到。

上面的示例应改写如下

  • 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 时,您在可以使用的数据类型方面不受限制。例如,您的 Bean 可能有一个包含多个属性的自定义 record 作为构造函数参数。

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

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

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

Spring AOT 检测创建 Bean 需要执行的操作,并使用实例供应商将其转换为生成的代码。容器还支持使用自定义参数创建 Bean,这会导致 AOT 的一些问题

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

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

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

避免循环依赖

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

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

FactoryBean

FactoryBean 应谨慎使用,因为它在 Bean 类型解析方面引入了中间层,这在概念上可能没有必要。根据经验,如果 FactoryBean 实例不保存长期状态,并且在运行时的稍后时间点不需要,则应将其替换为常规工厂方法,可能在顶部添加 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 实例的回调。可以使用任何 Spring Bean 或 @Bean 工厂方法上的 @ImportRuntimeHints 注解来注册此接口的实现。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 会被考虑,但您可以选择使用 @ReflectiveScan 进行扫描。在下面的示例中,com.example.app 包及其子包中的所有类型都会被考虑。

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ReflectiveScan;

@Configuration
@ReflectiveScan("com.example.app")
public class MyConfiguration {
}

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

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

库作者可以将其注解用于自己的目的。下一节将介绍这种自定义的一个示例。

@RegisterReflection

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

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

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

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

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

@RegisterReflection 可以用作元注解来提供更具体的需要。@RegisterReflectionForBinding 就是这样一个组合注解,它注册了序列化任意类型的需求。一个典型的用例是在方法体中使用 Web 客户端时使用容器无法推断的 DTO。

以下示例注册 Order 用于序列化。

@Component
class OrderService {

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

}

这会为 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 代理,这是一个 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.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 文件中找到更多详细信息。