此版本仍在开发中,尚不被认为是稳定的。对于最新的稳定版本,请使用 Spring Framework 6.2.10! |
提前优化
本章介绍 Spring 的提前 (AOT) 优化。
有关特定于集成测试的 AOT 支持,请参阅 测试的提前支持。
提前优化简介
Spring 对 AOT 优化的支持旨在检查ApplicationContext
在构建时应用通常在运行时发生的决策和发现逻辑。
这样做可以构建一个更直接的应用程序启动安排,并专注于主要基于类路径和Environment
.
尽早应用此类优化意味着以下限制:
-
类路径是固定的,并且在构建时完全定义。
-
应用程序中定义的 bean 不能在运行时更改,这意味着:
-
@Profile
,特别是特定于配置文件的配置,需要在构建时选择,并在启用 AOT 时在运行时自动启用。 -
Environment
影响 Bean (@Conditional
) 仅在构建时考虑。
-
-
无法提前转换具有实例提供者(lambda 或方法引用)的 Bean 定义。
-
注册为单例的 Bean(使用
registerSingleton
,通常来自ConfigurableListableBeanFactory
)也不能提前转换。 -
由于我们不能依赖实例,因此请确保 bean 类型与 可能。
另请参阅最佳实践部分。 |
当这些限制到位时,就可以在构建时执行提前处理并生成其他资产。 Spring AOT 处理的应用程序通常会生成:
-
Java 源代码
-
字节码(通常用于动态代理)
-
RuntimeHints
用于使用反射、资源加载、序列化和 JDK 代理
目前,AOT 专注于允许使用 GraalVM 将 Spring 应用程序部署为原生映像。 我们打算在未来几代人中支持更多基于 JVM 的用例。 |
AOT 发动机概述
AOT 引擎的入口点,用于处理ApplicationContext
是ApplicationContextAotGenerator
.它根据GenericApplicationContext
表示要优化的应用程序和GenerationContext
:
-
刷新
ApplicationContext
用于AOT处理。与传统的刷新相反,此版本仅创建 Bean 定义,而不创建 Bean 实例。 -
调用可用的
BeanFactoryInitializationAotProcessor
实施并应用其贡献GenerationContext
. 例如,核心实现迭代所有候选 bean 定义并生成必要的代码来恢复BeanFactory
.
此过程完成后,GenerationContext
将使用应用程序运行所需的生成代码、资源和类进行更新。
这RuntimeHints
实例还可用于生成相关的 GraalVM 本机映像配置文件。
ApplicationContextAotGenerator#processAheadOfTime
返回ApplicationContextInitializer
允许使用 AOT 优化启动上下文的入口点。
以下部分将更详细地介绍这些步骤。
AOT 处理的刷新
所有 AOT 处理的刷新都支持GenericApplicationContext
实现。
应用程序上下文是使用任意数量的入口点创建的,通常采用@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
作为只有豆子
定义被考虑在内。一个好的模式是实现ImportBeanDefinitionRegistrar
并通过@Import
在您的其中一个
配置类。
因为这种模式实际上并没有创建 bean 实例,BeanPostProcessor
不调用实现,但与 AOT 处理相关的特定变体除外。
这些都是:
-
MergedBeanDefinitionPostProcessor
实现后处理 Bean 定义以提取其他设置,例如init
和destroy
方法。 -
SmartInstantiationAwareBeanPostProcessor
如有必要,实现确定更精确的 bean 类型。 这可以确保创建运行时所需的任何代理。
完成此部分后,BeanFactory
包含运行应用程序所需的 Bean 定义。它不会触发 bean 实例化,但允许 AOT 引擎检查将在运行时创建的 bean。
Bean 工厂初始化 AOT 贡献
想要参与此步骤的组件可以实现BeanFactoryInitializationAotProcessor
接口。
每个实现都可以根据 bean 工厂的状态返回 AOT 贡献。
AOT 贡献是贡献生成代码的组件,该代码重现了特定行为。
它还可以做出贡献RuntimeHints
以指示需要反射、资源加载、序列化或 JDK 代理。
一个BeanFactoryInitializationAotProcessor
实现可以在META-INF/spring/aot.factories
键等于接口的完全限定名称。
这BeanFactoryInitializationAotProcessor
接口也可以直接由 bean 实现。
在此模式下,bean 提供与它为常规运行时提供的功能相当的 AOT 贡献。
因此,此类 bean 会自动从 AOT 优化上下文中排除。
如果 bean 实现了 |
Bean 注册 AOT 贡献
核心BeanFactoryInitializationAotProcessor
执行负责为每个候选人收集必要的贡献BeanDefinition
.
它使用专用的BeanRegistrationAotProcessor
.
该接口的使用方式如下:
-
由
BeanPostProcessor
bean,以替换其运行时行为。 例如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
一个用于dataSourceBean
.
当datasource
instance 是必需的,则BeanInstanceSupplier
被称为。
此提供商调用dataSource()
方法dataSourceConfiguration
豆。
使用 AOT 优化运行
AOT 是将 Spring 应用程序转换为本机可执行文件的强制性步骤,因此它
在本机映像中运行时自动启用。但是,也可以使用 AOT 优化
在 JVM 上,通过设置spring.aot.enabled
System 属性设置为true
.
当包含 AOT 优化时,在生成时做出的一些决策 在应用程序设置中硬编码。例如,已在 构建时间也会在运行时自动启用。 |
最佳实践
AOT 引擎旨在处理尽可能多的用例,无需更改应用程序中的代码。 但是,请记住,一些优化是在构建时根据 Bean 的静态定义进行的。
本部分列出了确保应用程序已准备好使用 AOT 的最佳做法。
程序化 Bean 注册
AOT 引擎负责@Configuration
model 和任何可能的回调
作为处理配置的一部分调用。如果您需要额外注册
bean 中,请确保使用BeanDefinitionRegistry
注册
bean 定义。
这通常可以通过BeanDefinitionRegistryPostProcessor
.请注意,如果它
本身注册为 bean,它将在运行时再次调用,除非您将
一定要实现BeanFactoryInitializationAotProcessor
也。一个更惯用的
方式是实现ImportBeanDefinitionRegistrar
并使用@Import
上
配置类之一。这会在配置中调用自定义代码
类解析。
如果您使用不同的回调以编程方式声明其他 bean,则它们是 可能不会由 AOT 引擎处理,因此不会有任何提示 为他们生成。根据环境的不同,这些 Bean 可能不会在 都。例如,类路径扫描在本机映像中不起作用,因为没有 类路径的概念。对于此类情况,扫描必须在 构建时间。
公开最精确的 Bean 类型
虽然您的应用程序可能会与 Bean 实现的接互,但声明最精确的类型仍然非常重要。
AOT 引擎对 bean 类型执行其他检查,例如检测@Autowired
members 或生命周期回调方法。
为@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
如有必要,首选。
如果您正在处理无法修改的代码库,则可以将preferredConstructors
属性在相关的 bean 定义上指示应该使用哪个构造函数。
避免构造函数参数和属性的复杂数据结构
制作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 需要执行的作,并将其转换为使用实例提供者的生成代码。 容器还支持创建具有自定义参数的 bean,这可能会导致 AOT 出现几个问题:
-
自定义参数需要动态内省匹配的构造函数或工厂方法。 AOT 无法检测到这些参数,因此必须手动提供必要的反射提示。
-
绕过实例提供者意味着创建后的所有其他优化也会被跳过。 例如,字段和方法的自动装配将被跳过,因为它们是在实例提供商中处理的。
我们建议使用手动工厂模式,其中 Bean 负责创建实例,而不是使用自定义参数创建原型范围的 Bean。
避免循环依赖关系
某些用例可能会导致一个或多个 bean 之间出现循环依赖关系。使用
常规运行时,可以通过以下方式连接这些循环依赖项@Autowired
在 setter 方法或字段上。但是,AOT 优化的上下文将无法以
显式循环依赖关系。
因此,在 AOT 优化的应用程序中,应努力避免循环
依赖。如果无法做到这一点,您可以使用@Lazy
注入点或ObjectProvider
延迟访问或检索必要的协作 Bean。有关更多信息,请参阅此提示。
工厂豆
FactoryBean
应谨慎使用,因为它在 bean 类型分辨率方面引入了一个中间层,这在概念上可能不是必需的。
根据经验,如果FactoryBean
实例不保持长期状态,并且在运行时稍后不需要,它应该替换为常规@Bean
factory 方法,可能带有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 运行时相比,将应用程序作为本机映像运行需要额外的信息。例如,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
实现允许您获得对RuntimeHints
由
AOT发动机。可以使用以下命令注册此接口的实现@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
应尽可能靠近需要提示的组件使用。
这样,如果组件没有贡献给BeanFactory
,提示也不会被贡献。
也可以通过在META-INF/spring/aot.factories
键等于RuntimeHintsRegistrar
接口。
@Reflective
@Reflective
提供了一种惯用方式来标记需要对带注释的元素进行反射。
例如@EventListener
元注释为@Reflective
由于底层实现使用反射调用带注释的方法。
开箱即用,仅考虑 Spring bean,但您可以使用@ReflectiveScan
.在
下面的示例,所有类型中的com.example.app
package 及其子包是
考虑:
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
可以应用于类级别的任何目标类型,但也可以直接应用于方法,以更好地指示实际需要提示的位置。
@RegisterReflection
可以用作元注释以支持更具体的需求。@RegisterReflectionForBinding
是一个组合的注释,用元注释@RegisterReflection
并记录序列化任意类型的需要。
一个典型的用例是使用容器无法推断的 DTO,例如在方法体中使用 Web 客户端。
以下示例寄存器Order
用于序列化。
@Component
class OrderService {
@RegisterReflectionForBinding(Order.class)
public void process(Order order) {
// ...
}
}
这会注册构造函数、字段、属性和记录组件的提示Order
.
提示也为属性和记录组件上可传递使用的类型注册。
换句话说,如果Order
公开其他类型,也为这些类型注册提示。
基于约定的转换的运行时提示
虽然核心容器内置了对许多 常见类型(参见 Spring Type Conversion),一些 通过依赖于反射的基于约定的算法支持转换。
具体来说,如果没有显式Converter
注册ConversionService
对于特定的源→目标类型对,内部ObjectToObjectConverter
将尝试使用约定将源对象转换为目标类型,方法是委托
到源对象上的方法或静态工厂方法或构造函数
目标类型。由于这种基于约定的算法可以应用于
runtime 时,核心容器无法推断出支持
这样的反思。
如果您在原生映像中遇到基于约定的转换问题,则该问题导致
如果缺少运行时提示,您可以通过编程方式注册必要的提示。为
例如,如果您的应用程序需要从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
,一个用于检查现有提示是否与特定用例匹配的实用程序。
这可以在您自己的测试中使用,以验证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 代理,这是一个 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);
}
}
如果您忘记贡献提示,测试将失败并提供有关调用的一些详细信息:
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 框架buildSrc
自述文件文件。