对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

预先优化

本章介绍 Spring 的预先编译(AOT)优化。spring-doc.cadn.net.cn

有关集成测试的 AOT 支持,请参阅测试的提前编译(Ahead of Time)支持spring-doc.cadn.net.cn

AOT(提前)优化简介

Spring 对 AOT(提前编译)优化的支持旨在在构建时检查 ApplicationContext,并应用通常在运行时才执行的决策和发现逻辑。 这样做可以构建一个更直接、更聚焦的应用启动配置,该配置主要基于类路径和 Environment,并针对一组固定的功能进行优化。spring-doc.cadn.net.cn

过早应用此类优化意味着以下限制:spring-doc.cadn.net.cn

  • 类路径在构建时即已固定并完全定义。spring-doc.cadn.net.cn

  • 您应用程序中定义的 Bean 在运行时无法更改,这意味着:spring-doc.cadn.net.cn

    • @Profile,特别是特定于配置文件的配置,需要在构建时进行选择,并在启用 AOT 时于运行时自动启用。spring-doc.cadn.net.cn

    • 影响 Bean 存在性的 Environment 属性(@Conditional)仅在构建时被考虑。spring-doc.cadn.net.cn

  • 使用实例供应者(lambda 表达式或方法引用)的 Bean 定义无法被提前转换。spring-doc.cadn.net.cn

  • 以单例形式注册的 Bean(使用 registerSingleton 方法,通常来自 ConfigurableListableBeanFactory)也无法进行提前转换。spring-doc.cadn.net.cn

  • 由于我们无法依赖该实例,请确保 Bean 类型尽可能精确。spring-doc.cadn.net.cn

另请参阅最佳实践部分。

当这些限制生效时,便可以在构建时执行预先处理(ahead-of-time processing)并生成额外的资源文件。 经过 Spring AOT 处理的应用程序通常会生成:spring-doc.cadn.net.cn

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

AOT 引擎概述

AOT 引擎处理 ApplicationContext 的入口点是 ApplicationContextAotGenerator。它基于一个代表待优化应用的 GenericApplicationContext 和一个 GenerationContext,负责执行以下步骤:spring-doc.cadn.net.cn

  • 为 AOT 处理刷新一个 ApplicationContext。与传统的刷新不同,此版本仅创建 Bean 定义,而不创建 Bean 实例。spring-doc.cadn.net.cn

  • 调用所有可用的 BeanFactoryInitializationAotProcessor 实现,并将其贡献应用到 GenerationContext 上。 例如,一个核心实现会遍历所有候选的 bean 定义,并生成必要的代码以恢复 BeanFactory 的状态。spring-doc.cadn.net.cn

一旦此过程完成,GenerationContext 将被更新,包含应用程序运行所需的生成代码、资源和类。 RuntimeHints 实例也可用于生成相关的 GraalVM 原生镜像配置文件。spring-doc.cadn.net.cn

ApplicationContextAotGenerator#processAheadOfTime 返回 ApplicationContextInitializer 入口点的类名,该入口点允许使用 AOT 优化来启动上下文。spring-doc.cadn.net.cn

这些步骤在下面的章节中有更详细的说明。spring-doc.cadn.net.cn

刷新以进行 AOT 处理

所有 GenericApplicationContext 实现都支持用于 AOT 处理的刷新操作。 应用程序上下文可通过任意数量的入口点创建,通常以带有 @Configuration 注解的类的形式存在。spring-doc.cadn.net.cn

让我们来看一个基本示例:spring-doc.cadn.net.cn

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

使用常规运行时启动此应用程序涉及多个步骤,包括类路径扫描、配置类解析、Bean 实例化以及生命周期回调处理。 专为 AOT 处理执行的刷新操作仅应用了 常规 refresh 中部分流程。 可按以下方式触发 AOT 处理:spring-doc.cadn.net.cn

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

在此模式下,BeanFactoryPostProcessor 实现 会照常调用。 这包括配置类解析、导入选择器、类路径扫描等。 这些步骤确保 BeanRegistry 包含应用程序相关的 Bean 定义。 如果 Bean 定义受条件保护(例如 @Profile),则会对这些条件进行评估, 不满足条件的 Bean 定义将在此阶段被丢弃。spring-doc.cadn.net.cn

如果自定义代码需要以编程方式注册额外的 Bean,请确保自定义注册代码使用 BeanDefinitionRegistry 而不是 BeanFactory,因为只有 Bean 定义会被考虑在内。一种良好的做法是实现 ImportBeanDefinitionRegistrar 接口,并通过在某个配置类上使用 @Import 注解来注册它。spring-doc.cadn.net.cn

由于此模式实际上并不创建 Bean 实例,因此不会调用 BeanPostProcessor 的实现,除非是与 AOT 处理相关的特定变体。 这些变体包括:spring-doc.cadn.net.cn

  • MergedBeanDefinitionPostProcessor 的实现类会对 bean 定义进行后处理,以提取额外的设置,例如 initdestroy 方法。spring-doc.cadn.net.cn

  • SmartInstantiationAwareBeanPostProcessor 的实现会在必要时确定更精确的 bean 类型。 这确保了在运行时创建所需的任何代理。spring-doc.cadn.net.cn

一旦此阶段完成,BeanFactory 就包含了应用程序运行所需的 bean 定义。它不会触发 bean 的实例化,但允许 AOT 引擎检查将在运行时创建的 bean。spring-doc.cadn.net.cn

Bean Factory 初始化 AOT 贡献

希望参与此步骤的组件可以实现 BeanFactoryInitializationAotProcessor 接口。 每个实现都可以根据 Bean 工厂的状态返回一个 AOT 贡献。spring-doc.cadn.net.cn

AOT 贡献(AOT contribution)是一个组件,它贡献生成的代码以复现特定行为。 它还可以贡献 RuntimeHints,用以表明需要反射、资源加载、序列化或 JDK 代理。spring-doc.cadn.net.cn

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

BeanFactoryInitializationAotProcessor 接口也可以由一个 bean 直接实现。 在此模式下,该 bean 提供的 AOT 贡献与其在常规运行时所提供的功能等效。 因此,此类 bean 会自动从 AOT 优化的上下文中排除。spring-doc.cadn.net.cn

如果一个 Bean 实现了 BeanFactoryInitializationAotProcessor 接口,那么该 Bean 及其所有依赖项都将在 AOT(Ahead-of-Time)处理期间被初始化。 我们通常建议仅由基础设施 Bean(例如 BeanFactoryPostProcessor)实现此接口,因为这类 Bean 的依赖较少,并且在 Bean 工厂生命周期的早期阶段就已被初始化。 如果使用 @Bean 工厂方法注册此类 Bean,请确保该方法是 static 的,以避免其所在的 @Configuration 类必须被初始化。spring-doc.cadn.net.cn

Bean 注册的 AOT 贡献

一个核心的 BeanFactoryInitializationAotProcessor 实现负责收集每个候选 BeanDefinition 所需的贡献内容。 它通过一个专用的 BeanRegistrationAotProcessor 来实现这一点。spring-doc.cadn.net.cn

该接口的使用方式如下:spring-doc.cadn.net.cn

  • 由一个 BeanPostProcessor Bean 实现,用于替换其运行时行为。 例如,AutowiredAnnotationBeanPostProcessor 实现了此接口,以生成注入带有 @Autowired 注解的成员的代码。spring-doc.cadn.net.cn

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

如果一个 Bean 实现了 BeanRegistrationAotProcessor 接口,那么该 Bean 及其所有依赖项都将在 AOT(Ahead-of-Time)处理期间被初始化。 我们通常建议仅由基础设施 Bean(例如 BeanFactoryPostProcessor)实现此接口,因为这类 Bean 的依赖较少,并且在 Bean 工厂生命周期的早期阶段就已被初始化。 如果使用 @Bean 工厂方法注册此类 Bean,请确保该方法是 static 的,以避免其所在的 @Configuration 类必须被初始化。spring-doc.cadn.net.cn

如果没有 BeanRegistrationAotProcessor 处理某个已注册的 bean,则会由一个默认实现来处理它。 这是默认行为,因为针对 bean 定义调整生成的代码应仅限于特殊情况。spring-doc.cadn.net.cn

以我们之前的示例为例,假设 DataSourceConfiguration 如下所示:spring-doc.cadn.net.cn

@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 引擎会将上述配置类转换为类似于以下的代码:spring-doc.cadn.net.cn

/**
 * 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。 该 supplier 会在 dataSource() bean 上调用 dataSourceConfiguration 方法。spring-doc.cadn.net.cn

使用 AOT 优化运行

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

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

最佳实践

AOT 引擎旨在处理尽可能多的使用场景,而无需对应用程序代码进行任何更改。 然而,请注意,某些优化是在构建时基于 Bean 的静态定义进行的。spring-doc.cadn.net.cn

本节列出了确保您的应用程序已准备好支持AOT的最佳实践。spring-doc.cadn.net.cn

编程式 Bean 注册

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

这通常可以通过 BeanDefinitionRegistryPostProcessor 来实现。请注意,如果它本身被注册为一个 bean,除非你同时确保实现了 BeanFactoryInitializationAotProcessor,否则它将在运行时再次被调用。一种更为惯用的方式是实现 ImportBeanDefinitionRegistrar 接口,并通过在你的某个配置类上使用 @Import 注解来注册它。这样,你的自定义代码将在解析配置类的过程中被调用。spring-doc.cadn.net.cn

如果你使用不同的回调以编程方式声明额外的 Bean,它们很可能不会被 AOT 引擎处理,因此不会为这些 Bean 生成任何提示(hints)。根据运行环境的不同,这些 Bean 甚至可能根本不会被注册。例如,在原生镜像(native image)中,类路径扫描(classpath scanning)无法工作,因为原生镜像中没有类路径的概念。对于此类情况,在构建时(build time)执行扫描至关重要。spring-doc.cadn.net.cn

暴露最精确的 Bean 类型

虽然您的应用程序可能会与某个 Bean 所实现的接口进行交互,但声明最精确的类型仍然非常重要。 AOT 引擎会对 Bean 类型执行额外的检查,例如检测是否存在 @Autowired 成员或生命周期回调方法。spring-doc.cadn.net.cn

对于 @Configuration 类,请确保 @Bean 工厂方法的返回类型尽可能精确。 请考虑以下示例:spring-doc.cadn.net.cn

@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 处理期间将无法检测到该方法。spring-doc.cadn.net.cn

因此,上面的示例应重写如下:spring-doc.cadn.net.cn

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

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

}
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

	@Bean
	fun myInterface() = MyImplementation()

}

如果你以编程方式注册 Bean 定义,请考虑使用 RootBeanBefinition,因为它允许指定一个处理泛型的 ResolvableTypespring-doc.cadn.net.cn

避免多个构造函数

容器能够根据多个候选构造函数选择最合适的构造函数来使用。 然而,依赖这种机制并不是最佳实践,如有必要,建议使用 @Autowired 注解明确标记首选的构造函数。spring-doc.cadn.net.cn

如果您正在处理一个无法修改的代码库,可以在相关的 Bean 定义上设置 preferredConstructors 属性,以指定应使用哪个构造函数。spring-doc.cadn.net.cn

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

在以编程方式创建 RootBeanDefinition 时,您在可使用的类型方面不受限制。 例如,您可以拥有一个自定义的 record,其中包含多个属性,而您的 bean 可以将其作为构造函数参数。spring-doc.cadn.net.cn

虽然这种方式在常规运行时可以正常工作,但 AOT(提前编译)并不知道如何为您的自定义数据结构生成代码。 一个良好的经验法则是要记住:Bean 定义是建立在多种模型之上的抽象。 建议不要使用此类结构,而是将其分解为简单类型,或引用一个以这种方式构建的 Bean。spring-doc.cadn.net.cn

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

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

Spring AOT 会检测创建一个 Bean 所需执行的操作,并将其转换为使用实例供应器(instance supplier)的生成代码。 该容器还支持使用自定义参数创建 Bean,这可能会导致 AOT 出现若干问题:spring-doc.cadn.net.cn

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

  2. 绕过实例供应器(instance supplier)意味着创建之后的所有其他优化也会被跳过。 例如,字段和方法上的自动装配(autowiring)将被跳过,因为它们是在实例供应器中处理的。spring-doc.cadn.net.cn

与其使用带有自定义参数创建的原型作用域(prototype-scoped)Bean,我们建议采用手动工厂模式,即由一个 Bean 负责实例的创建。spring-doc.cadn.net.cn

避免循环依赖

某些使用场景可能会导致一个或多个 Bean 之间出现循环依赖。在常规运行时,可以通过在 setter 方法或字段上使用 @Autowired 注解来解决这些循环依赖。然而,经过 AOT 优化的上下文在存在显式循环依赖的情况下将无法启动。spring-doc.cadn.net.cn

因此,在 AOT 优化的应用程序中,您应尽量避免循环依赖。如果无法避免,您可以使用 @Lazy 注入点或 ObjectProvider 来延迟访问或获取所需的协作 Bean。更多信息请参见此提示spring-doc.cadn.net.cn

工厂 Bean

FactoryBean 应谨慎使用,因为它在 Bean 类型解析方面引入了一个中间层,而这个中间层在概念上可能并非必要。 通常来说,如果一个 FactoryBean 实例不持有长期状态,并且在运行时后续阶段也不再需要,那么就应该将其替换为一个普通的 @Bean 工厂方法,如有必要,可以在其上添加一个 FactoryBean 适配器层(用于声明式配置目的)。spring-doc.cadn.net.cn

如果你的 FactoryBean 实现没有解析对象类型(即 T),则需要格外小心。 请考虑以下示例:spring-doc.cadn.net.cn

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

具体的客户端声明应为客户端提供一个已解析的泛型,如下例所示:spring-doc.cadn.net.cn

@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 定义,请确保遵循以下步骤:spring-doc.cadn.net.cn

  1. 使用 RootBeanDefinitionspring-doc.cadn.net.cn

  2. beanClass 设置为 FactoryBean 类,以便 AOT 能够识别它是一个中间层。spring-doc.cadn.net.cn

  3. ResolvableType 设置为一个已解析的泛型,以确保暴露最精确的类型。spring-doc.cadn.net.cn

以下示例展示了一个基本的定义:spring-doc.cadn.net.cn

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 持久化单元必须提前已知。请考虑以下基本示例:spring-doc.cadn.net.cn

@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 定义中使用它,如下例所示:spring-doc.cadn.net.cn

@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 运行时相比,将应用程序作为原生镜像(native image)运行需要额外的信息。 例如,GraalVM 需要提前知道某个组件是否使用了反射。 同样,除非显式指定,否则类路径资源不会包含在原生镜像中。 因此,如果应用程序需要加载某个资源,则必须在相应的 GraalVM 原生镜像配置文件中引用该资源。spring-doc.cadn.net.cn

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

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

在AOT处理过程中,许多契约会自动处理。 例如,@Controller方法的返回类型会被检查,如果Spring检测到该类型需要被序列化(通常为JSON格式),就会自动添加相关的反射提示。spring-doc.cadn.net.cn

对于核心容器无法推断的情况,你可以通过编程方式注册此类提示。 同时也提供了一些便捷的注解,用于常见的使用场景。spring-doc.cadn.net.cn

@ImportRuntimeHints

RuntimeHintsRegistrar 实现允许您获取由 AOT 引擎管理的 RuntimeHints 实例的回调。此接口的实现可以使用 @ImportRuntimeHints 在任何 Spring Bean 或 @Bean 工厂方法上进行注册。RuntimeHintsRegistrar 实现会在构建时被检测并调用。spring-doc.cadn.net.cn

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 注解用在最靠近需要这些提示(hints)的组件处。 这样,如果该组件未被注册到 BeanFactory 中,相应的提示也不会被注册。spring-doc.cadn.net.cn

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

@Reflective

@Reflective 提供了一种符合习惯的方式,用于标记需要对注解元素进行反射。 例如,@EventListener@Reflective 元注解,因为其底层实现通过反射调用被注解的方法。spring-doc.cadn.net.cn

默认情况下,仅考虑 Spring Bean,但您可以选择使用 @ReflectiveScan 进行扫描。在 以下示例中,com.example.app 包及其子包中的所有类型都将被考虑:spring-doc.cadn.net.cn

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

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

扫描在AOT处理期间进行,目标包中的类型无需具有类级别的注解即可被纳入考虑。 这将执行一次深度扫描,并检查类型、字段、构造函数、方法及其内部元素上是否直接或通过元注解方式存在@Reflective注解。spring-doc.cadn.net.cn

默认情况下,@Reflective 会为被注解的元素注册一个调用提示。 这可以通过在 ReflectiveProcessor 注解中指定自定义的 @Reflective 实现来进行调整。spring-doc.cadn.net.cn

库的作者可以出于自己的目的重用此注解。 下一节将介绍此类自定义的一个示例。spring-doc.cadn.net.cn

@RegisterReflection

@RegisterReflection@Reflective 的一个特化,提供了一种声明式的方式来为任意类型注册反射。spring-doc.cadn.net.cn

作为 @Reflective 的一种特化形式,如果你使用了 @RegisterReflection@ReflectiveScan 也会被检测到。

在下面的示例中,可以通过反射调用 AccountService 的公共构造函数和公共方法:spring-doc.cadn.net.cn

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

@RegisterReflection 可以在类级别应用于任何目标类型,但也可以直接应用于方法,以更明确地指示实际需要提示(hints)的位置。spring-doc.cadn.net.cn

@RegisterReflection 可用作元注解,以支持更具体的需求。 @RegisterReflectionForBinding 是一个组合注解,它使用 @RegisterReflection 进行元注解标注,并注册了对任意类型序列化的需求。 典型用例是使用容器无法推断的 DTO(数据传输对象),例如在方法体内使用 Web 客户端。spring-doc.cadn.net.cn

以下示例将 Order 注册用于序列化。spring-doc.cadn.net.cn

@Component
class OrderService {

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

}

这会为 Order 的构造函数、字段、属性和记录组件注册提示信息。 同时,也会为其属性和记录组件中传递使用的类型注册提示信息。 换句话说,如果 Order 暴露了其他类型,这些类型也会被注册提示信息。spring-doc.cadn.net.cn

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

尽管核心容器为许多常见类型的自动转换提供了内置支持(参见Spring 类型转换),但某些转换是通过一种基于约定的算法实现的,该算法依赖于反射。spring-doc.cadn.net.cn

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

如果你在原生镜像(native image)中遇到由于缺少运行时提示(runtime hints)而导致的基于约定的转换问题,可以以编程方式注册所需的提示。例如,如果你的应用程序需要将 java.time.Instant 转换为 java.sql.Timestamp,并且依赖 ObjectToObjectConverter 通过反射调用 java.sql.Timestamp.from(Instant) 方法,那么你可以实现一个自定义的 RuntimeHintsRegitrar 来支持原生镜像中的这一使用场景,如下例所示。spring-doc.cadn.net.cn

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 配置文件以静态方式注册。spring-doc.cadn.net.cn

上述 TimestampConversionRuntimeHints 类是框架中包含的 ObjectToObjectConverterRuntimeHints 类的简化版本,并且默认已注册。spring-doc.cadn.net.cn

因此,这种特定的 InstantTimestamp 的使用场景已经由框架处理好了。spring-doc.cadn.net.cn

测试运行时提示

Spring Core 还提供了 RuntimeHintsPredicates,这是一个用于检查现有提示(hints)是否匹配特定使用场景的工具类。 你可以在自己的测试中使用它来验证 RuntimeHintsRegistrar 是否生成了预期的结果。 我们可以为 SpellCheckService 编写一个测试,以确保在运行时能够成功加载词典:spring-doc.cadn.net.cn

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

使用 RuntimeHintsPredicates,我们可以检查反射、资源、序列化或代理生成相关的提示。 这种方法适用于单元测试,但前提是组件的运行时行为是明确已知的。spring-doc.cadn.net.cn

通过使用GraalVM 跟踪代理运行应用程序的测试套件(或应用程序本身),您可以更深入地了解应用程序的全局运行时行为。 该代理将在运行时记录所有需要 GraalVM 提示的相关调用,并将其输出为 JSON 配置文件。spring-doc.cadn.net.cn

为了实现更有针对性的发现和测试,Spring Framework 提供了一个专用模块,其中包含核心 AOT 测试工具:"org.springframework:spring-core-test"。 该模块包含 RuntimeHints Agent(运行时提示代理),这是一个 Java 代理,用于记录所有与运行时提示相关的方法调用,并帮助你验证给定的 RuntimeHints 实例是否覆盖了所有已记录的调用。 让我们考虑一段基础设施代码,我们希望在 AOT 处理阶段对其所贡献的提示进行测试。spring-doc.cadn.net.cn

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

}

然后,我们可以编写一个单元测试(无需原生编译)来检查我们提供的提示:spring-doc.cadn.net.cn

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

}

如果你忘记提供一个提示(hint),测试将会失败,并提供有关该调用的一些详细信息:spring-doc.cadn.net.cn

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 文件中找到更多详细信息。spring-doc.cadn.net.cn