预先优化

本章涵盖Spring的提前编译(AOT)优化。spring-doc.cadn.net.cn

有关集成测试的特定AOT支持,请参阅测试的AOT支持spring-doc.cadn.net.cn

提前优化介绍

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

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

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

  • 作为单例注册的Beans(使用registerSingleton,通常来自 ConfigurableListableBeanFactory)也不能提前进行转换。spring-doc.cadn.net.cn

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

另请参阅最佳实践部分。

当这些限制存在时,便可在构建时执行提前处理并生成额外的资源文件。 经过Spring AOT处理的应用通常会产生:spring-doc.cadn.net.cn

目前,AOT 主要致力于支持 Spring 应用程序通过 GraalVM 部署为原生镜像。 我们计划在后续版本中支持更多基于 JVM 的使用场景。

AOT引擎概述

用于处理 ApplicationContext 的 AOT 引擎入口点是 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工厂初始化AOT贡献

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

AOT 贡献是一个组件,它贡献生成的代码以重现特定行为。 它还可以贡献 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处理期间被初始化。 我们通常建议此接口仅由基础设施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处理期间被初始化。 我们通常建议此接口仅由基础设施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。 此提供商调用dataSource()方法在dataSourceConfiguration bean上。spring-doc.cadn.net.cn

使用AOT优化运行

AOT是将Spring应用程序转换为本机可执行文件的必要步骤,因此在本机映像中运行时会自动启用。但也可以通过将spring.aot.mode系统属性设置为native,在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 可能根本不会被注册。例如,在原生镜像中,类路径扫描无法工作,因为不存在类路径的概念。对于此类情况,至关重要的是扫描必须在构建时发生。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 所需的操作,并将其转化为使用实例提供商的生成代码。 容器还支持使用自定义参数创建 bean,这可能会导致与 AOT 相关的几个问题:spring-doc.cadn.net.cn

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

  2. 跳过实例提供者意味着创建后的所有其他优化也会被跳过。 例如,字段和方法上的自动装配将被跳过,因为它们由实例提供者处理。spring-doc.cadn.net.cn

与其使用自定义参数创建原型作用域的 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,如下例所示: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运行时相比,以原生镜像方式运行应用程序需要额外的信息。 例如,GraalVM需要提前知道某个组件是否使用了反射。 同样,类路径资源除非显式指定,否则不会包含在原生镜像中。 因此,如果应用程序需要加载某个资源,则必须在相应的GraalVM原生镜像配置文件中引用它。spring-doc.cadn.net.cn

RuntimeHints API在运行时整合了反射、资源加载、序列化及JDK代理的需求。 以下示例确保在原生镜像中运行时能从类路径加载config/app.propertiesspring-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。 这样,如果该组件未贡献给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 为注解元素注册了一个调用提示。这可以通过使用 @Reflective 注解指定自定义的 ReflectiveProcessor 实现来调整。spring-doc.cadn.net.cn

库作者可以为了自己的目的重用此注解。这种自定义的一个示例在下一部分中有所涵盖。spring-doc.cadn.net.cn

@RegisterReflection

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

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

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

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

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

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

以下示例为序列化注册了Orderspring-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

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

如果在原生镜像中遇到因缺少运行时提示而导致的基于约定的转换问题,您可以以编程方式注册必要的提示。例如,如果您的应用程序需要从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,一个用于检查现有提示是否符合特定用例的工具。 您可以在自己的测试中使用它来验证 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框架提供了一个专用模块,其中包含核心的AOT测试实用工具"org.springframework:spring-core-test"。 该模块内置RuntimeHints代理(一个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.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 = org.springframework.aot.test.agent.RuntimeHintsRecorder.record(() -> {
			SampleReflection sample = new SampleReflection();
			sample.performReflection();
		});
		// assert that the recorded invocations are covered by the contributed hints
		assertThat(invocations).match(runtimeHints);
	}

}

如果您忘记提供提示,测试将失败并显示有关该调用的详细信息: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