https://docs.spring.io/spring-framework/reference/core/aot.html
提前优化简介
Spring对AOT优化的支持旨在在构建时检查ApplicationContext
,并应用通常在运行时发生的决策和发现逻辑。这样做允许构建一个更直接且专注于基于类路径和Environment
的一组固定特性的应用程序启动安排。
提前应用此类优化意味着以下限制:
- 类路径在构建时是固定且完全定义的。
- 在应用程序中定义的bean在运行时不能更改,这意味着:
1)@Profile
,特别是特定于profile的配置,需要在构建时进行选择。
2)影响bean存在性(@Conditional
)的Environment
性仅在构建时考虑。 - 带有实例提供者(lambda表达式或方法引用)的bean定义无法提前转换。
- 注册为单例的bean(使用
registerSingleton
,通常来自ConfigurableListableBeanFactory
)也无法提前转换。 - 由于无法依赖实例,请确保bean类型尽可能精确。
当这些限制生效时,就可以在构建时执行提前(Ahead-of-Time,AOT)处理,并生成额外的资源。一个经过Spring AOT处理的应用程序通常会生成:
- Java源代码
- 字节码(通常用于动态代理)
- 运行时提示(
RuntimeHints
),用于反射、资源加载、序列化和JDK代理的使用
目前,AOT主要集中在允许Spring应用程序使用GraalVM部署为原生镜像。打算在未来的版本中支持更多基于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处理的刷新仅应用了常规刷新中发生的一个子集。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工厂初始化AOT贡献
想要参与此步骤的组件可以实现BeanFactoryInitializationAotProcessor
接口。每个实现都可以根据bean工厂的状态返回一个AOT贡献。
AOT贡献是一个生成代码的组件,这些代码可以复现特定行为。它还可以贡献RuntimeHints
,以指示对反射、资源加载、序列化或JDK代理的需求。
一个BeanFactoryInitializationAotProcessor
实现可以在META-INF/spring/aot.factories
中注册,键等于接口的完全限定名。
BeanFactoryInitializationAotProcessor
接口也可以直接由bean实现。在这种模式下,bean提供的AOT贡献相当于它在常规运行时提供的功能。因此,这样的bean会自动从AOT优化的上下文中排除。
如果一个bean实现了BeanFactoryInitializationAotProcessor
接口,那么在AOT处理期间,该bean及其所有依赖项都将被初始化。通常建议只有像BeanFactoryPostProcessor
这样的基础设施bean实现这个接口,因为它们的依赖项有限,并且已经在bean工厂生命周期早期初始化了。如果这样的bean是使用@Bean
工厂方法注册的,请确保该方法是静态的,这样它的封装@Configuration
类就不必初始化了。
Bean注册的AOT贡献
一个核心的BeanFactoryInitializationAotProcessor
实现负责收集每个候选BeanDefinition
所需的贡献。它通过使用专用的BeanRegistrationAotProcessor
来完成这项工作。
这个接口的使用方法如下:
- 由一个
BeanPostProcessor
bean实现,以替换其运行时行为。例如,AutowiredAnnotationBeanPostProcessor
实现了这个接口,用于生成注入带有@Autowired
注解的成员的代码。 - 由在
META-INF/spring/aot.factories
中注册的类型实现,键等于接口的完全限定名。通常用于需要针对核心框架的特定功能调整bean定义时。
如果一个bean实现了BeanRegistrationAotProcessor
接口,那么在AOT处理期间,该bean及其所有依赖项都将被初始化。通常建议只有像BeanFactoryPostProcessor
这样的基础设施bean实现这个接口,因为它们的依赖项有限,并且已经在bean工厂生命周期早期初始化了。如果这样的bean是使用@Bean
工厂方法注册的,请确保该方法是静态的,这样它的封装@Configuration
类就不必初始化了。
如果没有BeanRegistrationAotProcessor
处理特定的已注册bean,将由默认实现来处理它。这是默认行为,因为调整bean定义的生成代码应该仅限于特殊情况。
举例,假设DataSourceConfiguration
如下:
@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {
@Bean
public SimpleDataSource dataSource() {
return new SimpleDataSource();
}
}
由于这个类没有任何特定条件,dataSourceConfiguration
和dataSource
被识别为候选者。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;
}
}
生成的确切代码可能会根据bean定义的确切性质而有所不同。
每个生成的类都使用org.springframework.aot.generate.Generated
注解进行标记,以便在需要排除时(例如通过静态分析工具)识别它们。
上面生成的代码创建了与@Configuration
类等效的bean定义,但是以直接的方式,并且尽可能不使用反射。有一个dataSourceConfiguration
的bean定义和一个dataSourceBean
的定义。当需要datasource
实例时,会调用BeanInstanceSupplier
。这个提供者在dataSourceConfiguration
bean上调用dataSource()
方法。
最佳实践
AOT引擎旨在在应用程序中无需代码更改的情况下处理尽可能多的用例。然而,基于bean的静态定义,一些优化是在构建时进行的。
程序化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();
}
}
在上面的示例中,myInterface
bean的声明类型是MyInterface
。通常的后处理不会考虑MyImplementation
。例如,如果MyImplementation
上有一个应该由上下文注册的带注解的handler 方法,它将不会被预先检测到。
上面的示例应该重写如下:
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {
@Bean
public MyImplementation myInterface() {
return new MyImplementation();
}
}
如果你正在以编程方式注册bean定义,请考虑使用RootBeanBefinition
,因为它允许指定处理泛型的ResolvableType
。
免多个构造函数
容器能够根据几个候选构造函数选择最合适的构造函数来使用。然而,这不是一个最佳实践,如果必要的话,最好使用@Autowired
标记首选构造函数。
如果你正在处理一个无法修改的代码库,可以在相关的bean定义上设置preferredConstructors
属性,以指示应使用哪个构造函数。
FactoryBean
应该谨慎使用FactoryBean
,因为它在bean类型解析方面引入了一个中间层,这在概念上可能是不必要的。作为一条经验法则,如果FactoryBean
实例不持有长期状态,并且在运行时的后续时间点不需要,那么它应该被一个常规的工厂方法替换,可能在上面有一个FactoryBean
适配器层(用于声明性配置目的)。
如果你的FactoryBean
实现不解析对象类型(即T
),则需要格外小心。考虑以下示例:
public class ClientFactoryBean<T extends AbstractClient> implements FactoryBean<T> {
// ...
}
具体的客户端声明应该为客户端提供一个已解析的泛型,如下例所示:
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {
@Bean
public ClientFactoryBean<MyClient> myClient() {
return new ClientFactoryBean<>(...);
}
}
如果FactoryBean
bean定义是程序化注册的,请确保遵循以下步骤:
- 使用
RootBeanDefinition
。 - 将
beanClass
设置为FactoryBean
类,以便AOT知道它是一个中间层。 - 将
ResolvableType
设置为已解析的泛型,以确保公开最精确的类型。
以下示例展示了一个基本定义:
RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class);
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class));
// ...
registry.registerBeanDefinition("myClient", beanDefinition);
JPA
JPA持久化单元必须提前知道,以便应用某些优化。考虑以下基本示例:
@Bean
LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setPackagesToScan("com.example.app");
return factoryBean;
}
为了确保提前进行扫描,必须声明并使用PersistenceManagedTypes
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;
}
运行时提示
将应用程序作为原生镜像运行与常规JVM运行时相比需要额外的信息。例如,GraalVM需要提前知道组件是否使用反射。同样,类路径资源不会包含在原生镜像中,除非明确指定。因此,如果应用程序需要加载资源,必须从相应的GraalVM原生镜像配置文件中引用。
RuntimeHints
API收集运行时对反射、资源加载、序列化和JDK代理的需求。以下示例确保在原生镜像中运行时可以从类路径加载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
应该尽可能靠近需要提示的组件使用。这样,如果组件没有贡献到BeanFactory
,提示也不会被贡献。
也可以通过在META-INF/spring/aot.factories
中添加一个条目来静态注册实现,键等于RuntimeHintsRegistrar
接口的完全限定名。
@Reflective
@Reflective
提供了一种惯用的方式来标记对注解元素需要反射。例如,@EventListener
使用@Reflective
元注解,因为底层实现使用反射调用被注解的方法。
默认情况下,只考虑Spring bean,并为注解元素注册一个调用提示。可以通过@Reflective
注解指定自定义的ReflectiveProcessor
实现来调整这种行为。
库作者可以重用此注解以实现自己的目标。如果需要处理除Spring bean之外的组件,BeanFactoryInitializationAotProcessor
可以检测相关类型并使用ReflectiveRuntimeHintsRegistrar
处理它们。
@RegisterReflectionForBinding
@RegisterReflectionForBinding
是@Reflective
的一个特定用途版本,它注册了序列化任意类型的需求。一个典型的用例是在方法体内部使用Web客户端时,容器无法推断的DTO的使用。
@RegisterReflectionForBinding
可以应用于任何Spring bean的类级别,但也可以直接应用于方法、字段或构造函数,以更好地指示实际需要提示的位置。以下示例为序列化注册了Account
。
@Component
public class OrderService {
@RegisterReflectionForBinding(Account.class)
public void process(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);
}
使用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.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代理有各种方法。代理本身可以配置为仅针对特定包进行插桩(默认情况下,只有org.springframework
被插桩)。