预先优化
本章介绍 Spring 的预先编译(AOT)优化。
有关集成测试的 AOT 支持,请参阅测试的提前编译(Ahead of Time)支持。
AOT(提前)优化简介
Spring 对 AOT(提前编译)优化的支持旨在在构建时检查 ApplicationContext,并应用通常在运行时才执行的决策和发现逻辑。
这样做可以构建一个更直接、更聚焦的应用启动配置,该配置主要基于类路径和 Environment,并针对一组固定的功能进行优化。
过早应用此类优化意味着以下限制:
-
类路径在构建时即已固定并完全定义。
-
您应用程序中定义的 Bean 在运行时无法更改,这意味着:
-
@Profile,特别是特定于配置文件的配置,需要在构建时进行选择,并在启用 AOT 时于运行时自动启用。 -
影响 Bean 存在性的
Environment属性(@Conditional)仅在构建时被考虑。
-
-
使用实例供应者(lambda 表达式或方法引用)的 Bean 定义无法被提前转换。
-
以单例形式注册的 Bean(使用
registerSingleton方法,通常来自ConfigurableListableBeanFactory)也无法进行提前转换。 -
由于我们无法依赖该实例,请确保 Bean 类型尽可能精确。
| 另请参阅最佳实践部分。 |
当这些限制生效时,便可以在构建时执行预先处理(ahead-of-time processing)并生成额外的资源文件。 经过 Spring AOT 处理的应用程序通常会生成:
-
Java 源代码
-
字节码(通常用于动态代理)
-
RuntimeHints用于反射、资源加载、序列化和 JDK 动态代理
| 目前,AOT 专注于允许使用 GraalVM 将 Spring 应用程序部署为原生镜像。 我们计划在未来版本中支持更多基于 JVM 的使用场景。 |
AOT 引擎概述
AOT 引擎处理 ApplicationContext 的入口点是 ApplicationContextAotGenerator。它基于一个代表待优化应用的 GenericApplicationContext 和一个 GenerationContext,负责执行以下步骤:
-
为 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 定义进行后处理,以提取额外的设置,例如init和destroy方法。 -
SmartInstantiationAwareBeanPostProcessor的实现会在必要时确定更精确的 bean 类型。 这确保了在运行时创建所需的任何代理。
一旦此阶段完成,BeanFactory 就包含了应用程序运行所需的 bean 定义。它不会触发 bean 的实例化,但允许 AOT 引擎检查将在运行时创建的 bean。
Bean Factory 初始化 AOT 贡献
希望参与此步骤的组件可以实现 BeanFactoryInitializationAotProcessor 接口。
每个实现都可以根据 Bean 工厂的状态返回一个 AOT 贡献。
AOT 贡献(AOT contribution)是一个组件,它贡献生成的代码以复现特定行为。
它还可以贡献 RuntimeHints,用以表明需要反射、资源加载、序列化或 JDK 代理。
可以在 BeanFactoryInitializationAotProcessor 中注册一个 META-INF/spring/aot.factories 实现,其键为该接口的全限定名。
BeanFactoryInitializationAotProcessor 接口也可以由一个 bean 直接实现。
在此模式下,该 bean 提供的 AOT 贡献与其在常规运行时所提供的功能等效。
因此,此类 bean 会自动从 AOT 优化的上下文中排除。
|
如果一个 Bean 实现了 |
Bean 注册的 AOT 贡献
一个核心的 BeanFactoryInitializationAotProcessor 实现负责收集每个候选 BeanDefinition 所需的贡献内容。
它通过一个专用的 BeanRegistrationAotProcessor 来实现这一点。
该接口的使用方式如下:
-
由一个
BeanPostProcessorBean 实现,用于替换其运行时行为。 例如,AutowiredAnnotationBeanPostProcessor实现了此接口,以生成注入带有@Autowired注解的成员的代码。 -
由在
META-INF/spring/aot.factories中注册的类型实现,其键等于该接口的全限定名。 通常在需要针对核心框架的特定功能调整 bean 定义时使用。
|
如果一个 Bean 实现了 |
如果没有 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 类名。 |
由于该类没有任何特定条件,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.generate.Generated 注解,以便在需要排除这些类时(例如被静态分析工具排除)能够识别它们。 |
上面生成的代码创建了与 @Configuration 类等效的 bean 定义,但采用了一种直接的方式,并且在可能的情况下完全不使用反射。
其中包含一个 dataSourceConfiguration 的 bean 定义和一个 dataSourceBean 的 bean 定义。
当需要一个 datasource 实例时,会调用一个 BeanInstanceSupplier。
该 supplier 会在 dataSource() bean 上调用 dataSourceConfiguration 方法。
使用 AOT 优化运行
AOT 是将 Spring 应用程序转换为原生可执行文件的必要步骤,因此在原生镜像(native image)中运行时会自动启用。
不过,也可以通过将 spring.aot.enabled 系统属性设置为 true,在 JVM 上使用 AOT 优化。
| 当包含AOT优化时,一些在构建时做出的决策会被硬编码到应用程序的配置中。例如,在构建时启用的配置文件也会在运行时自动启用。 |
最佳实践
AOT 引擎旨在处理尽可能多的使用场景,而无需对应用程序代码进行任何更改。 然而,请注意,某些优化是在构建时基于 Bean 的静态定义进行的。
本节列出了确保您的应用程序已准备好支持AOT的最佳实践。
编程式 Bean 注册
AOT 引擎负责处理 @Configuration 模型以及在处理您的配置过程中可能被调用的任何回调。如果您需要以编程方式注册额外的 Bean,请务必使用 BeanDefinitionRegistry 来注册 Bean 定义。
这通常可以通过 BeanDefinitionRegistryPostProcessor 来实现。请注意,如果它本身被注册为一个 bean,除非你同时确保实现了 BeanFactoryInitializationAotProcessor,否则它将在运行时再次被调用。一种更为惯用的方式是实现 ImportBeanDefinitionRegistrar 接口,并通过在你的某个配置类上使用 @Import 注解来注册它。这样,你的自定义代码将在解析配置类的过程中被调用。
如果你使用不同的回调以编程方式声明额外的 Bean,它们很可能不会被 AOT 引擎处理,因此不会为这些 Bean 生成任何提示(hints)。根据运行环境的不同,这些 Bean 甚至可能根本不会被注册。例如,在原生镜像(native image)中,类路径扫描(classpath scanning)无法工作,因为原生镜像中没有类路径的概念。对于此类情况,在构建时(build time)执行扫描至关重要。
暴露最精确的 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 所需执行的操作,并将其转换为使用实例供应器(instance supplier)的生成代码。 该容器还支持使用自定义参数创建 Bean,这可能会导致 AOT 出现若干问题:
-
自定义参数需要对匹配的构造函数或工厂方法进行动态内省。 这些参数无法被 AOT 检测到,因此必须手动提供必要的反射提示。
-
绕过实例供应器(instance supplier)意味着创建之后的所有其他优化也会被跳过。 例如,字段和方法上的自动装配(autowiring)将被跳过,因为它们是在实例供应器中处理的。
与其使用带有自定义参数创建的原型作用域(prototype-scoped)Bean,我们建议采用手动工厂模式,即由一个 Bean 负责实例的创建。
避免循环依赖
某些使用场景可能会导致一个或多个 Bean 之间出现循环依赖。在常规运行时,可以通过在 setter 方法或字段上使用 @Autowired 注解来解决这些循环依赖。然而,经过 AOT 优化的上下文在存在显式循环依赖的情况下将无法启动。
因此,在 AOT 优化的应用程序中,您应尽量避免循环依赖。如果无法避免,您可以使用 @Lazy 注入点或 ObjectProvider 来延迟访问或获取所需的协作 Bean。更多信息请参见此提示。
工厂 Bean
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 定义,请确保遵循以下步骤:
-
使用
RootBeanDefinition。 -
将
beanClass设置为FactoryBean类,以便 AOT 能够识别它是一个中间层。 -
将
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 运行时相比,将应用程序作为原生镜像(native image)运行需要额外的信息。 例如,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 实现会在构建时被检测并调用。
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 中,相应的提示也不会被注册。
也可以通过在 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 会为被注解的元素注册一个调用提示。
这可以通过在 ReflectiveProcessor 注解中指定自定义的 @Reflective 实现来进行调整。
库的作者可以出于自己的目的重用此注解。 下一节将介绍此类自定义的一个示例。
@RegisterReflection
@RegisterReflection 是 @Reflective 的一个特化,提供了一种声明式的方式来为任意类型注册反射。
作为 @Reflective 的一种特化形式,如果你使用了 @RegisterReflection,@ReflectiveScan 也会被检测到。 |
在下面的示例中,可以通过反射调用 AccountService 的公共构造函数和公共方法:
@Configuration
@RegisterReflection(classes = AccountService.class, memberCategories =
{ MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS })
class MyConfiguration {
}
@RegisterReflection 可以在类级别应用于任何目标类型,但也可以直接应用于方法,以更明确地指示实际需要提示(hints)的位置。
@RegisterReflection 可用作元注解,以支持更具体的需求。
@RegisterReflectionForBinding 是一个组合注解,它使用 @RegisterReflection 进行元注解标注,并注册了对任意类型序列化的需求。
典型用例是使用容器无法推断的 DTO(数据传输对象),例如在方法体内使用 Web 客户端。
以下示例将 Order 注册用于序列化。
@Component
class OrderService {
@RegisterReflectionForBinding(Order.class)
public void process(Order order) {
// ...
}
}
这会为 Order 的构造函数、字段、属性和记录组件注册提示信息。
同时,也会为其属性和记录组件中传递使用的类型注册提示信息。
换句话说,如果 Order 暴露了其他类型,这些类型也会被注册提示信息。
基于约定的转换的运行时提示
尽管核心容器为许多常见类型的自动转换提供了内置支持(参见Spring 类型转换),但某些转换是通过一种基于约定的算法实现的,该算法依赖于反射。
具体来说,如果没有为特定的源类型 → 目标类型对在 Converter 中显式注册 ConversionService,内部的 ObjectToObjectConverter 将尝试使用约定,通过委托给源对象上的方法,或目标类型上的静态工厂方法或构造函数,将源对象转换为目标类型。由于这种基于约定的算法可在运行时应用于任意类型,因此核心容器无法推断出支持此类反射所需的运行时提示。
如果你在原生镜像(native image)中遇到由于缺少运行时提示(runtime hints)而导致的基于约定的转换问题,可以以编程方式注册所需的提示。例如,如果你的应用程序需要将 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 配置文件以静态方式注册。
|
上述 因此,这种特定的 |
测试运行时提示
Spring Core 还提供了 RuntimeHintsPredicates,这是一个用于检查现有提示(hints)是否匹配特定使用场景的工具类。
你可以在自己的测试中使用它来验证 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 代理,用于记录所有与运行时提示相关的方法调用,并帮助你验证给定的 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.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);
}
}
如果你忘记提供一个提示(hint),测试将会失败,并提供有关该调用的一些详细信息:
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 文件中找到更多详细信息。