本章介绍了 Spring 的提前 (AOT) 优化。
有关特定于集成测试的 AOT 支持,请参阅测试的提前支持。
提前优化简介
Spring 对 AOT 优化的支持旨在检查构建时的决策和发现逻辑,这些逻辑通常发生在运行时。
这样做可以构建一个更直接的应用程序启动安排,并专注于一组主要基于类路径和 .ApplicationContext
Environment
尽早应用此类优化意味着以下限制:
-
类路径是固定的,并在生成时完全定义。
-
应用程序中定义的 Bean 不能在运行时更改,这意味着:
-
@Profile
,特别是特定于配置文件的配置,需要在构建时选择,并在启用 AOT 时在运行时自动启用。 -
Environment
影响 Bean () 存在的属性仅在构建时考虑。@Conditional
-
-
无法提前转换具有实例供应商(lambda 或方法引用)的 Bean 定义。
-
注册为单例的 Bean (使用 ,通常来自 )也无法提前转换。
registerSingleton
ConfigurableListableBeanFactory
-
由于我们不能依赖实例,因此请确保 Bean 类型与 可能。
另请参阅“最佳实践”部分。 |
当这些限制到位时,就可以在构建时执行提前处理并生成额外的资产。 Spring AOT 处理的应用程序通常会生成:
-
Java 源代码
-
字节码(通常用于动态代理)
-
用于使用反射、资源加载、序列化和 JDK 代理的
RuntimeHints
目前,AOT 专注于允许使用 GraalVM 将 Spring 应用程序部署为本机映像。 我们打算在未来几代人中支持更多基于 JVM 的用例。 |
AOT 引擎概述
用于处理 的 AOT 引擎的入口点是 。它负责以下步骤,基于表示要优化的应用程序和 GenerationContext
:ApplicationContext
ApplicationContextAotGenerator
GenericApplicationContext
-
刷新 AOT 处理。与传统的刷新相反,此版本仅创建 Bean 定义,而不创建 Bean 实例。
ApplicationContext
-
调用可用的实现,并对 . 例如,核心实现遍历所有候选 Bean 定义,并生成必要的代码来恢复 .
BeanFactoryInitializationAotProcessor
GenerationContext
BeanFactory
此过程完成后,将使用应用程序运行所需的生成代码、资源和类进行更新。
该实例还可用于生成相关的 GraalVM 本机映像配置文件。GenerationContext
RuntimeHints
ApplicationContextAotGenerator#processAheadOfTime
返回允许使用 AOT 优化启动上下文的入口点的类名。ApplicationContextInitializer
以下各节更详细地介绍了这些步骤。
刷新 AOT 处理
所有实现都支持刷新 AOT 处理。
应用程序上下文是使用任意数量的入口点创建的,通常采用带注释的类的形式。GenericApplicationContext
@Configuration
让我们看一个基本的例子:
@Configuration(proxyBeanMethods=false)
@ComponentScan
@Import({DataSourceConfiguration.class, ContainerConfiguration.class})
public class MyApplication {
}
使用常规运行时启动此应用程序涉及许多步骤,包括类路径扫描、配置类解析、Bean 实例化和生命周期回调处理。
AOT 处理的刷新仅应用常规刷新
所发生情况的子集。
可以按如下方式触发 AOT 处理:
RuntimeHints hints = new RuntimeHints();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(MyApplication.class);
context.refreshForAotProcessing(hints);
// ...
context.close();
在此模式下,将像往常一样调用 BeanFactoryPostProcessor
实现。
这包括配置类解析、导入选择器、类路径扫描等。
这些步骤确保包含应用程序的相关 Bean 定义。
如果 Bean 定义受条件(例如 )保护,则对这些条件进行评估,
在此阶段,与其条件不匹配的 Bean 定义将被丢弃。BeanRegistry
@Profile
如果自定义代码需要以编程方式注册额外的 Bean,请确保自定义
注册码使用而不是仅作为 Bean
定义也被考虑在内。一个好的模式是通过 on one of your
配置类。BeanDefinitionRegistry
BeanFactory
ImportBeanDefinitionRegistrar
@Import
由于此模式实际上不会创建 Bean 实例,因此不会调用实现,但与 AOT 处理相关的特定变体除外。
这些是:BeanPostProcessor
-
MergedBeanDefinitionPostProcessor
实现后处理 Bean 定义以提取其他设置,例如 AND 方法。init
destroy
-
SmartInstantiationAwareBeanPostProcessor
如有必要,实现会确定更精确的 Bean 类型。 这样可以确保创建运行时所需的任何代理。
完成此部分后,将包含应用程序运行所需的 Bean 定义。它不会触发 Bean 实例化,但允许 AOT 引擎检查将在运行时创建的 Bean。BeanFactory
Bean 工厂初始化 AOT 贡献
想要参与此步骤的组件可以实现 BeanFactoryInitializationAotProcessor
接口。
每个实现都可以根据 Bean 工厂的状态返回一个 AOT 贡献。
AOT 贡献是贡献生成的代码的组件,该代码再现特定行为。
它还有助于指示对反射、资源加载、序列化或 JDK 代理的需求。RuntimeHints
可以使用等于接口的完全限定名称的密钥注册实现。BeanFactoryInitializationAotProcessor
META-INF/spring/aot.factories
该接口也可以直接由 Bean 实现。
在此模式下,Bean 提供的 AOT 贡献等同于它在常规运行时提供的功能。
因此,此类 Bean 会自动从 AOT 优化的上下文中排除。BeanFactoryInitializationAotProcessor
如果 Bean 实现了接口,则 Bean 及其所有依赖项将在 AOT 处理期间初始化。
我们通常建议此接口仅由基础结构 Bean 实现,例如具有有限依赖关系且已在 Bean 工厂生命周期早期初始化的 Bean。
如果使用工厂方法注册了这样的 Bean,请确保该方法不必初始化其封闭类。 |
Bean 注册 AOT 贡献
核心实现负责为每个候选人收集必要的贡献。
它使用专用的 .BeanFactoryInitializationAotProcessor
BeanDefinition
BeanRegistrationAotProcessor
此接口的用法如下:
-
由 Bean 实现,以替换其运行时行为。 例如,
AutowiredAnnotationBeanPostProcessor
实现此接口以生成注入带有 .注释的成员的代码。BeanPostProcessor
@Autowired
-
由注册的类型实现,该类型使用等于接口的完全限定名称的密钥。 通常在需要针对核心框架的特定功能调整 Bean 定义时使用。
META-INF/spring/aot.factories
如果 Bean 实现了接口,则 Bean 及其所有依赖项将在 AOT 处理期间初始化。
我们通常建议此接口仅由基础结构 Bean 实现,例如具有有限依赖关系且已在 Bean 工厂生命周期早期初始化的 Bean。
如果使用工厂方法注册了这样的 Bean,请确保该方法不必初始化其封闭类。 |
如果没有处理特定的注册 Bean,那么缺省实现将处理它。
这是默认行为,因为调整为 Bean 定义生成的代码应限制在极端情况下。BeanRegistrationAotProcessor
以前面的例子为例,我们假设如下: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 标识符(不以字母开头、包含空格等)。 |
因为这个班级没有任何特殊条件,并且被确定为候选人。
AOT 引擎会将上述配置类转换为类似于以下内容的代码:dataSourceConfiguration
dataSource
-
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 |
上面生成的代码创建了与类等效的 Bean 定义,但以直接的方式创建,如果可能的话,不使用反射。
有一个 bean 定义 和 一个 for 。
当需要实例时,将调用 a。
此供应商在 Bean 上调用该方法。@Configuration
dataSourceConfiguration
dataSourceBean
datasource
BeanInstanceSupplier
dataSource()
dataSourceConfiguration
使用 AOT 优化运行
AOT 是将 Spring 应用程序转换为本机可执行文件的强制性步骤,因此它
在此模式下运行时自动启用。可以使用这些优化
在 JVM 上,将 System 属性设置为 。spring.aot.enabled
true
当包括 AOT 优化时,在构建时做出的一些决策 在应用程序设置中硬编码。例如,已在 构建时也会在运行时自动启用。 |
最佳实践
AOT 引擎旨在处理尽可能多的用例,而无需更改应用程序中的代码。 但是,请记住,一些优化是在构建时基于 Bean 的静态定义进行的。
本部分列出了确保应用程序已准备好进行 AOT 的最佳做法。
程序化 Bean 注册
AOT 引擎负责处理模型和任何可能
在处理配置时调用。如果您需要注册其他
bean 以编程方式,请确保使用 a 进行注册
Bean 定义。@Configuration
BeanDefinitionRegistry
这通常可以通过 .请注意,如果它
将自身注册为 bean,它将在运行时再次调用,除非您使
一定要实施。一个更惯用的
方法是使用 ON 实现和注册它
您的配置类之一。这将在配置过程中调用自定义代码
类解析。BeanDefinitionRegistryPostProcessor
BeanFactoryInitializationAotProcessor
ImportBeanDefinitionRegistrar
@Import
如果您使用不同的回调以编程方式声明其他 Bean,则它们是 AOT 引擎可能不会处理,因此不会有任何提示 为他们生成。根据环境的不同,这些 Bean 可能不会在 都。例如,类路径扫描在本机映像中不起作用,因为没有 类路径的概念。对于此类情况,扫描必须在 构建时间。
公开最精确的 Bean 类型
虽然应用程序可能与 Bean 实现的接口进行交互,但声明最精确的类型仍然非常重要。
AOT 引擎对 Bean 类型执行其他检查,例如检测成员或生命周期回调方法的存在。@Autowired
对于类,请确保工厂方法的返回类型尽可能精确。
请看以下示例:@Configuration
@Bean
-
Java
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {
@Bean
public MyInterface myInterface() {
return new MyImplementation();
}
}
在上面的示例中,Bean 的声明类型为 。
通常的后处理都不会被考虑在内。
例如,如果上下文应注册带注释的处理程序方法,则不会预先检测到它。myInterface
MyInterface
MyImplementation
MyImplementation
上面的示例应按如下方式重写:
-
Java
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {
@Bean
public MyImplementation myInterface() {
return new MyImplementation();
}
}
如果以编程方式注册 Bean 定义,请考虑使用 as it allowd,以指定处理泛型的 a。RootBeanBefinition
ResolvableType
避免使用多个构造函数
容器能够根据多个候选项选择最合适的构造函数来使用。
但是,这不是最佳做法,最好在必要时标记首选构造函数。@Autowired
如果您正在处理无法修改的代码库,则可以在相关 Bean 定义上设置 preferredConstructors
属性,以指示应使用哪个构造函数。
避免构造函数参数和属性的复杂数据结构
以编程方式制作时,在可以使用的类型方面不受限制。
例如,您可能有一个自定义项,其中包含多个属性,您的 Bean 将这些属性用作构造函数参数。RootBeanDefinition
record
虽然这在常规运行时工作正常,但 AOT 不知道如何生成自定义数据结构的代码。 一个好的经验法则是记住,Bean 定义是建立在几个模型之上的抽象。 与其使用这种结构,不如将分解为简单的类型或引用以这种方式构建的 Bean。
作为最后的手段,您可以实现自己的.
要使用它,请在 using as the key 中注册其完全限定名称。org.springframework.aot.generate.ValueCodeGenerator$Delegate
META-INF/spring/aot.factories
Delegate
避免使用自定义参数创建 Bean
Spring AOT 检测创建 Bean 需要做什么,并使用实例供应商将其转换为生成的代码。 该容器还支持使用自定义参数创建 Bean,这会导致 AOT 出现几个问题:
-
自定义参数需要对匹配的构造函数或工厂方法进行动态自省。 AOT 无法检测到这些参数,因此必须手动提供必要的反射提示。
-
绕过实例供应商意味着创建后的所有其他优化也将被跳过。 例如,字段和方法上的自动布线将被跳过,因为它们在实例供应商中处理。
我们建议使用手动工厂模式,其中 Bean 负责创建实例,而不是使用自定义参数创建原型范围的 Bean。
工厂豆
FactoryBean
应谨慎使用,因为它在 Bean 类型分辨率方面引入了一个中间层,这在概念上可能不是必需的。
根据经验,如果实例不保持长期状态,并且在运行时的稍后时间点不需要它,则应将其替换为常规工厂方法,可能使用顶部的适配器层(用于声明性配置目的)。FactoryBean
FactoryBean
如果您的实现无法解析对象类型(即 ),则需要格外小心。
请看以下示例:FactoryBean
T
-
Java
public class ClientFactoryBean<T extends AbstractClient> implements FactoryBean<T> {
// ...
}
具体的客户端声明应为客户端提供解析的泛型,如以下示例所示:
-
Java
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {
@Bean
public ClientFactoryBean<MyClient> myClient() {
return new ClientFactoryBean<>(...);
}
}
如果 Bean 定义是以编程方式注册的,请确保按照下列步骤操作:FactoryBean
-
用。
RootBeanDefinition
-
将 设置为类,以便 AOT 知道它是一个中间层。
beanClass
FactoryBean
-
将 设置为已解析的泛型,以确保公开最精确的类型。
ResolvableType
以下示例展示了一个基本定义:
-
Java
RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class);
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class));
// ...
registry.registerBeanDefinition("myClient", beanDefinition);
JPA格式
必须预先知道 JPA 持久性单元,才能应用某些优化。请看以下基本示例:
-
Java
@Bean
LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setPackagesToScan("com.example.app");
return factoryBean;
}
为确保提前进行扫描,必须声明一个 Bean 并由
工厂 Bean 定义,如以下示例所示:PersistenceManagedTypes
-
Java
@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;
}
运行时提示
与常规 JVM 运行时相比,将应用程序作为本机映像运行需要额外的信息。 例如,GraalVM需要提前知道组件是否使用反射。 同样,除非显式指定,否则类路径资源不会包含在本机映像中。 因此,如果应用程序需要加载资源,则必须从相应的GraalVM本机映像配置文件中引用该资源。
RuntimeHints
API 在运行时收集对反射、资源加载、序列化和 JDK 代理的需求。
以下示例确保可以在运行时在本机映像中的类路径加载:config/app.properties
-
Java
runtimeHints.resources().registerPattern("config/app.properties");
在 AOT 处理期间会自动处理许多合同。
例如,检查方法的返回类型,如果 Spring 检测到该类型应序列化(通常为 JSON),则添加相关的反射提示。@Controller
对于核心容器无法推断的情况,可以通过编程方式注册此类提示。 还为常见用例提供了许多方便的注释。
@ImportRuntimeHints
RuntimeHintsRegistrar
通过实现,您可以获取对 AOT 引擎管理的实例的回调。
可以使用任何 Spring Bean 或工厂方法注册此接口的实现。 在生成时检测并调用实现。RuntimeHints
@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,并为带注释的元素注册调用提示。
这可以通过通过注释指定自定义实现来调整。ReflectiveProcessor
@Reflective
库作者可以出于自己的目的重复使用此批注。
如果需要处理除 Spring Bean 以外的组件,则可以检测相关类型并用于处理它们。BeanFactoryInitializationAotProcessor
ReflectiveRuntimeHintsRegistrar
@RegisterReflectionForBinding
@RegisterReflectionForBinding
是注册序列化任意类型的需要的专用化。
典型的用例是使用容器无法推断的 DTO,例如在方法主体中使用 Web 客户端。@Reflective
@RegisterReflectionForBinding
可以应用于类级别的任何 Spring Bean,但它也可以直接应用于方法、字段或构造函数,以更好地指示实际需要提示的位置。
以下示例注册序列化。Account
-
Java
@Component
public class OrderService {
@RegisterReflectionForBinding(Account.class)
public void process(Order order) {
// ...
}
}
测试运行时提示
Spring Core 还附带了一个实用程序,用于检查现有提示是否与特定用例匹配。
这可以在您自己的测试中用于验证 a 是否包含预期结果。
我们可以为我们编写一个测试,并确保我们能够在运行时加载字典: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 测试实用程序的专用模块。
此模块包含 RuntimeHints 代理,这是一个 Java 代理,用于记录与运行时提示相关的所有方法调用,并帮助您断言给定实例涵盖所有记录的调用。
让我们考虑一个基础设施,我们想测试我们在 AOT 处理阶段提供的提示。"org.springframework:spring-core-test"
RuntimeHints
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.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);
}
}
如果忘记提供提示,测试将失败,并提供有关调用的一些详细信息:
org.springframework.docs.core.aot.hints.testing.SampleReflection performReflection
INFO: Spring version:6.0.0-SNAPSHOT
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 代理,因此请参阅构建工具和测试执行插件的文档。
代理本身可以配置为检测特定包(默认情况下,仅检测)。
您可以在 Spring Framework buildSrc
README 文件中找到更多详细信息。org.springframework