提前优化
本章涵盖了Spring的“提前编译”(AOT)优化技术。
有关集成测试特定的AOT支持,请参阅针对测试的提前编译(AOT)支持。
预编译优化(Ahead of Time Optimizations)简介
Spring对AOT优化的支持旨在在构建时检查ApplicationContext,并应用通常在运行时进行的决策和发现逻辑。这样做可以构建一个更直接的应用启动方案,该方案主要基于类路径(classpath)和Environment来聚焦于一组固定的功能。
尽早应用此类优化意味着以下限制:
-
类路径在构建时被固定并完全定义。
-
在运行时,应用程序中定义的bean不能被更改,这意味着:
-
@Profile注解,特别是特定配置文件中的配置,需要在构建时选择;当启用了AOT(Ahead-Of-Time)时,这些配置会在运行时自动生效。 -
影响bean存在与否的
Environment属性(如@Conditional注解所依赖的属性)也仅在构建时被考虑。
-
-
包含实例供应者(lambda表达式或方法引用)的bean定义无法在构建前进行转换。
-
使用
registerSingleton注册为单例的bean(通常来自ConfigurableListableBeanFactory)也无法在构建前进行转换。 -
由于我们无法依赖运行时生成的实例,因此请确保bean类型尽可能精确。
另请参阅最佳实践部分。
当这些限制得以实施时,就能够在构建时进行提前处理并生成额外的资产。经过Spring AOT处理的应用程序通常会生成:
-
Java 源代码
-
字节码(通常用于动态代理)
-
RuntimeHints 用于反射、资源加载、序列化和 JDK 代理的使用
目前,AOT(Ahead-Of-Time)的重点是支持使用GraalVM将Spring应用程序部署为本机镜像。我们计划在未来的版本中支持更多基于JVM的用例。
AOT引擎概述
AOT引擎处理ApplicationContext的入口点是ApplicationContextAotGenerator。它基于代表要优化的应用程序的GenericApplicationContext和GenerationContext来执行以下步骤:
-
为AOT处理刷新
ApplicationContext。与传统刷新不同,此版本仅创建bean定义,而不创建bean实例。 -
调用可用的
BeanFactoryInitializationAotProcessor实现,并将其贡献应用于GenerationContext。例如,一个核心实现会遍历所有候选bean定义,并生成必要的代码以恢复BeanFactory的状态。
一旦这个过程完成,GenerationContext 将会被更新为包含应用程序运行所必需的生成代码、资源和类。RuntimeHints 实例也可以用来生成相关的 GraalVM 原生镜像配置文件。
ApplicationContextAotGenerator#processAheadOfTime 返回 ApplicationContextInitializer 入口点的类名,该入口点允许在应用启动时启用 AOT(Ahead-Of-Time)优化。
以下各部分将更详细地介绍这些步骤。
为AOT处理刷新
所有 GenericApplicationContext 的实现都支持用于 AOT 处理的刷新功能。应用程序上下文可以通过任意数量的入口点来创建,这些入口点通常以带有 @Configuration 注解的类的形式存在。
让我们来看一个基本的例子:
@Configuration(proxyBeanMethods=false)
@ComponentScan
@Import({DataSourceConfiguration.class, ContainerConfiguration.class})
public class MyApplication {
}
使用常规运行时启动此应用程序涉及多个步骤,包括类路径扫描、配置类解析、bean实例化以及生命周期回调处理。仅针对AOT处理的刷新(Refresh for AOT processing)仅适用于常规刷新中发生的一部分操作。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 定义进行后处理,以提取额外的设置,例如init和destroy方法。 -
SmartInstantiationAwareBeanPostProcessor的实现在必要时会确定更精确的 bean 类型。这样可以确保在运行时创建所需的任何代理。
一旦这部分完成,BeanFactory就包含了应用程序运行所必需的bean定义。它不会触发bean的实例化,但允许AOT引擎检查在运行时将要创建的bean。
Bean Factory 初始化的 AOT 贡献
希望参与此步骤的组件可以实现BeanFactoryInitializationAotProcessor接口。每个实现都可以根据bean工厂的状态返回一个AOT贡献(AOT contribution)。
AOT贡献(AOT contribution)是指一种组件,它提供生成的代码以再现特定的行为。该组件还可以提供RuntimeHints来指示是否需要反射(reflection)、资源加载(resource loading)、序列化(serialization)或JDK代理(JDK proxies)。
可以通过在META-INF/spring/aot.factories中注册一个BeanFactoryInitializationAotProcessor实现来使用它,该实现的键应与接口的全限定名称相同。
BeanFactoryInitializationAotProcessor接口也可以直接由一个bean来实现。在这种模式下,该bean提供的AOT功能等同于它在常规运行时所提供的功能。因此,这样的bean会自动被排除在AOT优化的上下文中。
如果一个bean实现了BeanFactoryInitializationAotProcessor接口,那么该bean及其所有依赖项都将在AOT处理期间被初始化。我们通常建议只有基础设施bean(如BeanFactoryPostProcessor)实现此接口,因为这类bean的依赖关系较少,并且在bean工厂的生命周期早期就已经被初始化了。如果使用@Bean工厂方法注册这样的bean,请确保该方法是static的,这样其所在的@Configuration类就不需要被初始化。
Bean Registration AOT Contributions
一个核心的BeanFactoryInitializationAotProcessor实现负责为每个候选的BeanDefinition收集必要的信息。它通过一个专用的BeanRegistrationAotProcessor来完成这一任务。
此接口的使用方法如下:
-
由一个
BeanPostProcessorbean实现,用于替换其运行时行为。例如[AutowiredAnnotationBeanPostProcessor](beans/factory-extension.md#beans-factory-extension-bpp-examples-aabpp)实现了该接口,以生成用于注入带有@Autowired注解的成员的代码。 -
由在
META-INF/spring/aot.factories中注册的类型实现,其键等于该接口的全限定名。通常在需要针对核心框架的特定功能调整 bean 定义时使用。
如果一个bean实现了BeanRegistrationAotProcessor接口,那么该bean及其所有依赖项都将在AOT处理期间被初始化。我们通常建议只有基础设施bean(如BeanFactoryPostProcessor)才实现此接口,因为这类bean的依赖关系有限,并且已经在bean工厂的生命周期早期就被初始化了。如果使用@Bean工厂方法注册这样的bean,请确保该方法是static的,这样其所在的@Configuration类就不需要被初始化了。
如果没有BeanRegistrationAotProcessor处理某个已注册的bean,那么将由默认实现来处理它。这是默认行为,因为针对bean定义调整生成的代码应该仅限于极端情况(corner cases)。
以我们之前的例子为例,假设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()
}
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.generateGenerated,以便在需要排除它们时(例如通过静态分析工具)能够识别出来。
上述生成的代码创建了与@Configuration类等效的bean定义,但这种方式是直接的,并且在可能的情况下完全不使用反射。有一个dataSourceConfiguration的bean定义,还有一个dataSourceBean的bean定义。当需要一个datasource实例时,会调用一个BeanInstanceSupplier。这个供应商会调用dataSourceConfiguration bean上的dataSource()方法。
使用AOT优化运行
AOT是将Spring应用程序转换为本地可执行文件的一个必要步骤,因此在运行在本地镜像(native image)中时,AOT会自动启用。不过,也可以通过将spring.aot.enabled系统属性设置为true来在JVM上使用AOT优化。
当包含AOT优化时,一些在构建时所做的决策会被硬编码到应用程序的设置中。例如,在构建时启用的一些配置文件也会在运行时自动被启用。
最佳实践
AOT(Ahead-Of-Time)引擎旨在在无需对应用程序进行任何代码修改的情况下,处理尽可能多的用例。然而,请记住,一些优化是基于beans的静态定义在构建时进行的。
本节列出了确保您的应用程序准备好进行AOT的最佳实践。
程序化Bean注册
AOT引擎会处理@Configuration模型以及在配置处理过程中可能被调用的任何回调函数。如果你需要以编程方式注册额外的bean,请确保使用BeanDefinitionRegistry来注册bean定义。
这通常可以通过 BeanDefinitionRegistryPostProcessor 来实现。需要注意的是,如果该处理器本身也被注册为一个 Bean,在运行时它还会被再次调用,除非你也确保实现了 BeanFactoryInitializationAotProcessor。一种更符合习惯的实现方式是实现 ImportBeanDefinitionRegistrar,并通过在某个配置类上使用 @Import 注解来注册它。这样,在解析配置类时就会调用你的自定义代码了。
如果你使用不同的回调机制以编程方式声明额外的bean,那么这些bean很可能不会被AOT(Ahead-Of-Time)引擎处理,因此也不会为它们生成任何提示信息。根据具体环境的不同,这些bean可能根本就不会被注册。例如,在原生镜像(native image)环境中,类路径扫描是无法使用的,因为那里并不存在类路径的概念。对于这样的情况,确保在构建时进行扫描就显得至关重要了。
暴露最精确的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定义的,可以考虑使用RootBeanDefinition,因为它允许指定一个能够处理泛型的ResolvableType。
避免使用多个构造函数
容器能够根据多个候选者选择最合适的构造函数来使用。然而,依赖这种方式并不是最佳实践,如果需要的话,最好用@Autowired来标记首选的构造函数。
如果你正在处理一个无法修改的代码库,你可以在相关的bean定义上设置preferredConstructors属性,以指示应使用哪个构造函数。
避免在构造函数参数和属性中使用复杂的数据结构
在通过编程方式创建RootBeanDefinition时,您在使用类型方面没有限制。例如,您可能有一个自定义的record,其中包含几个属性,而您的bean需要将这些属性作为构造函数参数。
虽然这在常规运行时中可以正常工作,但AOT(Ahead-Of-Time)并不知道如何生成你的自定义数据结构的代码。一个很好的经验法则是要记住,bean定义实际上是多种模型的抽象。因此,建议不要使用这样的结构,而是将其分解为简单的类型,或者引用专门构建为此目的的bean。
作为最后的手段,你可以实现自己的 org.springframework.aot.generate.ValueCodeGenerator$Delegate。要使用它,需要在 META-INF/spring/aot.factories 文件中注册其全限定名,以 org.springframework.aot.generate.ValueCodeGenerator$Delegate 作为键。
避免创建带有自定义参数的Bean
Spring AOT(编译时代码生成)会检测创建bean所需执行的操作,并将其转换成使用实例供应器的生成代码。容器还支持使用[自定义参数](https://docs.spring.io/spring-framework/docs/7.0.3/javadoc-api/org/springframework/beans/factory/BeanFactory.html#getBean(java.lang.String, java.lang.Object...))来创建bean,但这可能会导致AOT出现一些问题:
-
自定义参数需要动态地检查相应的构造函数或工厂方法。这些参数无法被AOT(Ahead-Of-Time编译)检测到,因此必须手动提供必要的反射提示。
-
绕过实例供应器意味着在创建之后的所有其他优化也会被跳过。例如,字段和方法的自动绑定也会被跳过,因为这些操作是在实例供应器中处理的。
与其使用带有自定义参数的原型作用域(prototype-scoped)bean来创建对象,我们推荐采用手动工厂模式(manual factory pattern),在这种模式下,由一个专门的bean负责创建实例。
避免循环依赖
在某些使用场景中,一个或多个bean之间可能会出现循环依赖。在常规运行时环境下,可以通过在setter方法或字段上使用@Autowired来处理这些循环依赖。然而,在经过AOT(Ahead-Of-Time)优化的环境中,如果存在显式的循环依赖,程序将无法启动。
在经过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>(...)
}
如果FactoryBeanbean定义是通过编程方式注册的,请确保遵循以下步骤:
- 使用
RootBeanDefinition。 - 将
beanClass设置为FactoryBean类,以便 AOT(Ahead-Of-Time Compilation)知道这是一个中间层。 - 将
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持久化单元(persistence unit)。考虑以下基本示例:
- 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
}
为了确保实体扫描能够提前进行,必须在工厂bean的定义中声明并使用一个PersistenceManagedTypes 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(Ahead-Of-Time)处理过程中,有许多合同(contracts)是自动处理的。例如,会检查@Controller方法的返回类型;如果Spring检测到该类型应该被序列化(通常为JSON格式),则会添加相关的反射提示(reflection hints)。
对于核心容器无法推断的情况,您可以以编程方式注册此类提示。还提供了一些方便的注解,适用于常见的使用场景。
@ImportRuntimeHints
RuntimeHintsRegistrar的实现允许你获得对AOT引擎管理的RuntimeHints实例的回调。可以通过在任何Spring bean上使用@ImportRuntimeHints或在@Bean工厂方法上来注册此接口的实现。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(Ahead-Of-Time)编译过程中,目标包中的类型无需带有类级别的注解也能被考虑在内。这种扫描是“深度扫描”(deep scan),会检查类型、字段、构造函数、方法以及包含在这些元素中的内容上是否存在@Reflective注解,无论是直接存在的还是作为元注解(meta-annotation)存在的。
默认情况下,@Reflective 会为被注解的元素注册一个调用提示(invocation hint)。通过使用 @Reflective 注解指定自定义的 ReflectiveProcessor 实现,可以对此进行调整。
图书馆作者可以根据自己的需求重新使用此注释。下一节将介绍此类自定义的一个示例。
@RegisterReflection
@RegisterReflection是@Reflective` 的一个特化版本,它提供了一种声明式的方法来为任意类型注册反射功能。
由于@RegisterReflection是@Reflective的特化形式,因此当您使用@ReflectiveScan时,也会检测到@RegisterReflection。
在以下示例中,可以通过对AccountService的反射来调用公共构造函数和公共方法:
@Configuration
@RegisterReflection(classes = AccountService.class, memberCategories =
{ MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS })
class MyConfiguration {
}
@RegisterReflection 可以在类级别应用于任何目标类型,但也可以直接应用于方法上,以便更清楚地指示实际需要提示的位置。
@RegisterReflection可以用作元注释,以支持更具体的需求。@RegisterReflectionForBinding(https://docs.spring.io/spring-framework/docs/7.0.3/javadoc-api/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.html)是一个复合注释,它被`@RegisterReflection`元注释所修饰,用于注册序列化任意类型的需求。一个典型的用例是使用容器无法推断的DTO(数据传输对象),例如在方法体内部使用Web客户端。
以下示例注册了Order以进行序列化。
@Component
class OrderService {
@RegisterReflectionForBinding(Order.class)
public void process(Order order) {
// ...
}
}
这会为Order的构造函数、字段、属性以及记录组件注册提示(hints)。对于在属性和记录组件上被递归使用的类型,也会为其注册提示。换句话说,如果Order暴露了其他类型,也会为这些类型注册提示。
基于约定的转换的运行时提示
尽管核心容器提供了对许多常见类型自动转换的内置支持(参见Spring 类型转换),但有些转换是通过基于约定的算法来支持的,这种算法依赖于反射(reflection)。
具体来说,如果对于某个特定的源类型到目标类型的对,ConversionService 中没有注册明确的 Converter,那么内部的 ObjectToObjectConverter 会尝试通过委派给源对象上的方法,或者目标类型上的静态工厂方法或构造函数来将源对象转换为目标类型。由于这种基于约定的算法可以在运行时应用于任意类型,因此核心容器无法推断出支持此类反射所需的运行时提示信息。
如果在本地镜像(native image)中遇到由于缺少运行时提示(runtime hints)而导致的基于约定的转换问题,你可以通过编程方式注册所需的提示。例如,如果你的应用程序需要将java.time.Instant转换为java.sql.Timestamp,并且依赖于ObjectToObjectConverter通过反射调用java.sqlTimestamp.from(Instant),那么你可以实现一个自定义的RuntimeHintsRegister来支持这种使用场景,如下例所示。
- 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 到 Timestamp 的转换用例已经由框架处理好了。
测试运行时提示
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 Agent,这是一个 Java 代理,它可以记录所有与运行时提示(runtime hints)相关的方法调用,并帮助你验证给定的 RuntimeHints 实例是否覆盖了所有被记录的调用。让我们来考虑一个基础设施示例,在这个示例中,我们想要测试在 AOT 处理阶段所引入的那些提示(hints)是否有效。
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.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);
}
}
如果你忘记提供提示,测试将会失败,并会提供关于调用的一些详细信息:
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文件中找到。