跳到主要内容

提前优化

ChatGPT-4o-mini 中英对照 Ahead of Time Optimizations

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

有关特定于集成测试的 AOT 支持,请参见 Ahead of Time Support for Tests

预先优化简介

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

应用此类优化的早期意味着以下限制:

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

  • 在您的应用程序中定义的 beans 在运行时不能更改,这意味着:

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

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

  • 带有实例供应者(lambda 或方法引用)的 bean 定义无法提前转换。

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

  • 由于我们不能依赖于实例,请确保 bean 类型尽可能精确。

提示

另请参见 最佳实践 部分。

当这些限制生效时,可以在构建时进行提前处理并生成额外的资产。一个经过 Spring AOT 处理的应用程序通常会生成:

  • Java 源代码

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

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

备注

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

AOT 引擎概述

AOT 引擎处理 ApplicationContext 的入口点是 ApplicationContextAotGenerator。它根据表示要优化的应用程序的 GenericApplicationContext 和一个 GenerationContext 处理以下步骤:

  • 刷新 ApplicationContext 以进行 AOT 处理。与传统的刷新不同,这个版本仅创建 bean 定义,而不创建 bean 实例。

  • 调用可用的 BeanFactoryInitializationAotProcessor 实现,并将它们的贡献应用于 GenerationContext。例如,一个核心实现遍历所有候选 bean 定义,并生成必要的代码以恢复 BeanFactory 的状态。

一旦这个过程完成,GenerationContext 将会更新为生成的代码、资源和应用程序运行所需的类。RuntimeHints 实例也可以用来生成相关的 GraalVM 原生镜像配置文件。

ApplicationContextAotGenerator#processAheadOfTime 返回允许上下文以 AOT 优化启动的 ApplicationContextInitializer 入口点的类名。

这些步骤在下面的章节中有更详细的说明。

AOT 处理的刷新

AOT 处理的刷新在所有 GenericApplicationContext 实现中都是支持的。应用程序上下文可以使用任意数量的入口点创建,通常以 @Configuration 注解的类的形式存在。

让我们看一个基本示例:

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

启动此应用程序的常规运行时涉及多个步骤,包括类路径扫描、配置类解析、Bean 实例化和生命周期回调处理。仅针对 AOT 处理的刷新只适用常规刷新中发生的一部分内容 regular refresh。AOT 处理可以通过以下方式触发:

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

在此模式下,BeanFactoryPostProcessor 实现会像往常一样被调用。这包括配置类解析、导入选择器、类路径扫描等。这些步骤确保 BeanRegistry 包含应用程序的相关 bean 定义。如果 bean 定义受到条件的保护(例如 @Profile),这些条件会被评估,不符合条件的 bean 定义将在此阶段被丢弃。

如果自定义代码需要以编程方式注册额外的 bean,请确保自定义注册代码使用 BeanDefinitionRegistry 而不是 BeanFactory,因为只有 bean 定义会被考虑。一个好的模式是实现 ImportBeanDefinitionRegistrar 并通过在你的配置类之一上使用 @Import 来注册它。

因为这种模式实际上并不创建 bean 实例,BeanPostProcessor 实现不会被调用,除了与 AOT 处理相关的特定变体。这些是:

  • MergedBeanDefinitionPostProcessor 实现类后处理 bean 定义以提取额外设置,例如 initdestroy 方法。

  • SmartInstantiationAwareBeanPostProcessor 实现类在必要时确定更精确的 bean 类型。这确保在运行时创建任何所需的代理。

一旦这一部分完成,BeanFactory 将包含应用程序运行所需的 bean 定义。它不会触发 bean 实例化,但允许 AOT 引擎检查将在运行时创建的 beans。

Bean Factory Initialization AOT Contributions

想要参与此步骤的组件可以实现 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 工厂方法注册的,请确保该方法是 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 工厂方法注册的,请确保该方法是 static,以便其封闭的 @Configuration 类不必被初始化。

如果没有 BeanRegistrationAotProcessor 处理特定的注册 bean,则会由默认实现来处理它。这是默认行为,因为对 bean 定义生成的代码进行调整应仅限于特殊情况。

以我们之前的例子为例,假设 DataSourceConfiguration 如下:

@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {

@Bean
public SimpleDataSource dataSource() {
return new SimpleDataSource();
}

}
java
注意

Kotlin 类名使用反引号且使用无效的 Java 标识符(不以字母开头、包含空格等)是不支持的。

由于这个类没有任何特定条件,dataSourceConfigurationdataSource 被识别为候选项。AOT 引擎将把上述配置类转换为类似于以下的代码:

/**
* 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;
}
}
java
备注

生成的确切代码可能会根据您的 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 方法的返回类型尽可能精确。考虑以下示例:

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

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

}
java

在上面的示例中,myInterface bean 的声明类型是 MyInterface。通常的后处理不会考虑 MyImplementation。例如,如果 MyImplementation 上有一个注解的处理方法需要被上下文注册,它将不会被提前检测到。

上面的示例应重写为如下:

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

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

}
java

如果您是以编程方式注册 bean 定义,请考虑使用 RootBeanDefinition,因为它允许指定一个处理泛型的 ResolvableType

避免多个构造函数

容器能够根据多个候选者选择最合适的构造函数。然而,这并不是最佳实践,如果需要,使用 @Autowired 标记首选构造函数是更好的选择。

如果您正在处理一个无法修改的代码库,可以在相关的 bean 定义上设置 preferredConstructors attribute 来指示应该使用哪个构造函数。

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

在程序上创建 RootBeanDefinition 时,您在可以使用的类型方面没有限制。例如,您可能有一个自定义的 record,它具有多个属性,您的 bean 将这些属性作为构造函数参数。

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

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

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

Spring AOT 检测到创建一个 bean 所需进行的操作,并使用实例供应商将其转换为生成的代码。容器还支持使用 自定义参数 创建一个 bean,这会导致 AOT 出现几个问题:

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

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

我们建议使用手动工厂模式,而不是使用带有自定义参数的原型作用域 bean。这样,bean 负责实例的创建。

避免循环依赖

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

在 AOT 优化的应用程序中,因此你应该努力避免循环依赖。如果这不可能,你可以使用 @Lazy 注入点或 ObjectProvider 来延迟访问或检索必要的协作 bean。有关更多信息,请参见 此提示

FactoryBean

FactoryBean 应谨慎使用,因为它在 bean 类型解析方面引入了一个中间层,这在概念上可能并不是必要的。作为一个经验法则,如果 FactoryBean 实例不持有长期状态,并且在运行时的后续阶段不需要它,则应将其替换为常规工厂方法,可能在其上方加一个 FactoryBean 适配器层(用于声明式配置目的)。

如果您的 FactoryBean 实现没有解析对象类型(即 T),则需要额外小心。请考虑以下示例:

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

一个具体的客户端声明应该为客户端提供一个解析后的泛型,如下例所示:

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

@Bean
public ClientFactoryBean<MyClient> myClient() {
return new ClientFactoryBean<>(...);
}

}
java

如果 FactoryBean 的 bean 定义是以编程方式注册的,请确保遵循以下步骤:

  1. 使用 RootBeanDefinition

  2. beanClass 设置为 FactoryBean 类,以便 AOT 知道它是一个中间层。

  3. ResolvableType 设置为已解析的泛型,这确保暴露最精确的类型。

以下示例展示了一个基本定义:

RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class);
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class));
// ...
registry.registerBeanDefinition("myClient", beanDefinition);
java

JPA

JPA 持久化单元必须提前知道,以便应用某些优化。考虑以下基本示例:

@Bean
LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setPackagesToScan("com.example.app");
return factoryBean;
}
java

为了确保扫描提前进行,必须声明一个 PersistenceManagedTypes bean,并在工厂 bean 定义中使用,如下例所示:

@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;
}
java

运行时提示

运行一个应用程序作为本地镜像相比于常规的 JVM 运行时需要额外的信息。例如,GraalVM 需要提前知道某个组件是否使用反射。同样,类路径资源不会被包含在本地镜像中,除非明确指定。因此,如果应用程序需要加载某个资源,它必须在相应的 GraalVM 本地镜像配置文件中被引用。

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

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

在 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/*");
}
}

}
java

如果可能的话,@ImportRuntimeHints 应该尽可能靠近需要提示的组件使用。这样,如果组件没有被贡献到 BeanFactory,提示也不会被贡献。

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

@Reflective

@Reflective 提供了一种惯用的方式来标记对注解元素进行反射的需求。例如,@EventListener 被元注解 @Reflective 注解,因为底层实现使用反射调用注解的方法。

开箱即用时,仅考虑 Spring beans,但您可以选择使用 @ReflectiveScan 进行扫描。在下面的示例中,com.example.app 包及其子包中的所有类型都会被考虑:

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

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

扫描发生在 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 {
}
java

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

@RegisterReflection 可以作为元注解使用,以提供更具体的需求。 @RegisterReflectionForBinding 是这样一个组合注解,用于注册序列化任意类型的需求。一个典型的用例是使用容器无法推断的 DTO,例如在方法体内使用网络客户端。

以下示例注册 Order 以进行序列化。

@Component
class OrderService {

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

}
java

这为 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);
}
java

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

}
java

我们可以编写一个单元测试(不需要本地编译)来检查我们贡献的提示:

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

}
java

如果您忘记提供提示,测试将失败并提供有关调用的一些详细信息:

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
txt

在您的构建中配置此 Java 代理有多种方法,因此请参考您构建工具和测试执行插件的文档。代理本身可以配置为对特定包进行插桩(默认情况下,仅对 org.springframework 进行插桩)。您可以在 Spring Framework buildSrc README 文件中找到更多详细信息。