对于最新的稳定版本,请使用 Spring Framework 6.2.4! |
提前优化
本章介绍了 Spring 的 Ahead of Time (AOT) 优化。
有关特定于集成测试的 AOT 支持,请参阅测试的提前支持。
Ahead of Time Optimization 简介
Spring 对 AOT 优化的支持旨在检查ApplicationContext
在构建时,应用通常在运行时发生的决策和发现逻辑。
这样做可以构建一个应用程序启动安排,该安排更直接,并专注于主要基于 Classpath 和Environment
.
提前应用此类优化意味着以下限制:
-
Classpath 是固定的,并且在构建时完全定义。
-
应用程序中定义的 bean 在运行时不能更改,这意味着:
-
@Profile
,特别是特定于配置文件的配置,需要在构建时选择,并在启用 AOT 时在运行时自动启用。 -
Environment
影响 Bean 存在的属性 (@Conditional
) 仅在构建时考虑。
-
-
具有实例提供者(lambda 或方法引用)的 Bean 定义不能提前转换。
-
注册为单例的 Bean (使用
registerSingleton
,通常来自ConfigurableListableBeanFactory
) 也无法提前转换。 -
由于我们不能依赖实例,因此请确保 bean 类型与 可能。
另请参阅 Best Practices 部分。 |
当这些限制到位时,就可以在构建时执行提前处理并生成其他资产。 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 {
}
使用常规运行时启动此应用程序涉及许多步骤,包括 Classpath 扫描、配置类解析、bean 实例化和生命周期回调处理。
AOT 处理的刷新仅应用定期refresh
.
AOT 处理可以按如下方式触发:
RuntimeHints hints = new RuntimeHints();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(MyApplication.class);
context.refreshForAotProcessing(hints);
// ...
context.close();
在此模式下,BeanFactoryPostProcessor
实现照常调用。
这包括 configuration class 解析、import selectors、classpath 扫描等。
这些步骤可确保BeanRegistry
包含应用程序的相关 Bean 定义。
如果 Bean 定义由条件(例如@Profile
)、这些被评估、
与条件不匹配的 bean 定义在此阶段被丢弃。
如果自定义代码需要以编程方式注册额外的 bean,请确保自定义
注册码用途BeanDefinitionRegistry
而不是BeanFactory
作为唯一的 Bean
定义被考虑在内。一个好的模式是实现ImportBeanDefinitionRegistrar
并通过@Import
在你的
configuration 类。
因为此模式实际上并不创建 bean 实例,所以BeanPostProcessor
不会调用实现,但与 AOT 处理相关的特定变体除外。
这些是:
-
MergedBeanDefinitionPostProcessor
实现对 bean 定义进行后处理以提取其他设置,例如init
和destroy
方法。 -
SmartInstantiationAwareBeanPostProcessor
如有必要,实现会确定更精确的 bean 类型。 这可确保创建运行时所需的任何代理。
完成此部分后,BeanFactory
包含应用程序运行所需的 Bean 定义。它不会触发 bean 实例化,但允许 AOT 引擎检查将在运行时创建的 bean。
Bean Factory 初始化 AOT 贡献
想要参与此步骤的组件可以实现BeanFactoryInitializationAotProcessor
接口。
每个实现都可以根据 bean 工厂的状态返回 AOT 贡献。
AOT 贡献是一个组件,它贡献了生成的代码,该代码可重现特定行为。
它还可以做出贡献RuntimeHints
以指示需要反射、资源加载、序列化或 JDK 代理。
一个BeanFactoryInitializationAotProcessor
implementation 可以在META-INF/spring/aot.factories
其键等于接口的完全限定名称。
这BeanFactoryInitializationAotProcessor
接口也可以由 bean 直接实现。
在此模式下,Bean 提供的 AOT 贡献相当于它通过常规运行时提供的功能。
因此,这样的 bean 会自动从 AOT 优化的上下文中排除。
如果 Bean 实现了 |
Bean 注册 AOT 贡献
一个核心BeanFactoryInitializationAotProcessor
implementation 负责为每个 candidate 收集必要的贡献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 应用程序转换为本机可执行文件的强制性步骤,因此它
在此模式下运行时会自动启用。可以使用这些优化
在 JVM 上,通过设置spring.aot.enabled
System 属性设置为true
.
当包含 AOT 优化时,在构建时做出的一些决策 在应用程序设置中进行硬编码。例如,已在 build-time 也会在运行时自动启用。 |
最佳实践
AOT 引擎旨在处理尽可能多的使用案例,而无需更改应用程序中的代码。 但是,请记住,一些优化是在构建时根据 bean 的静态定义进行的。
本节列出了确保您的应用程序已准备好进行 AOT 的最佳实践。
编程 Bean 注册
AOT 引擎负责@Configuration
model 和任何可能是
作为处理配置的一部分调用。如果您需要额外注册
bean 中,请确保使用BeanDefinitionRegistry
注册
bean 定义。
这通常可以通过BeanDefinitionRegistryPostProcessor
.请注意,如果它
将自身注册为 Bean,它将在运行时再次调用,除非你使
一定要实施BeanFactoryInitializationAotProcessor
也。一个更地道的
方法是实施ImportBeanDefinitionRegistrar
并使用@Import
上
您的配置类之一。这会在配置过程中调用您的自定义代码
类解析。
如果你使用不同的回调以编程方式声明其他 bean,那么它们是 可能不会由 AOT 引擎处理,因此不会有任何提示 为他们生成。根据环境,这些 bean 可能未在 都。例如,类路径扫描在本机映像中不起作用,因为没有 classpath 的概念。对于此类情况,扫描在 构建时间。
公开最精确的 Bean 类型
虽然您的应用程序可能与 Bean 实现的接口进行交互,但声明最精确的类型仍然非常重要。
AOT 引擎对 Bean 类型执行其他检查,例如检测是否存在@Autowired
members 或生命周期回调方法。
为@Configuration
类中,请确保 Factory 的 return 类型@Bean
方法尽可能精确。
请考虑以下示例:
-
Java
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {
@Bean
public MyInterface myInterface() {
return new MyImplementation();
}
}
在上面的示例中,myInterface
bean 是MyInterface
.
通常的后处理都不会MyImplementation
考虑。
例如,如果MyImplementation
上下文应该注册,则不会预先检测到它。
上面的示例应重写如下:
-
Java
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {
@Bean
public MyImplementation myInterface() {
return new MyImplementation();
}
}
如果要以编程方式注册 Bean 定义,请考虑使用RootBeanBefinition
因为它允许指定ResolvableType
处理泛型。
避免多个构造函数
容器能够根据多个候选项选择最合适的构造函数来使用。
但是,这不是最佳实践,使用@Autowired
如有必要,则首选。
如果您正在处理无法修改的代码库,则可以设置preferredConstructors
属性以指示应该使用哪个构造函数。
避免构造函数参数和属性使用复杂的数据结构
当制作RootBeanDefinition
从编程方式来看,您不受可以使用的类型的限制。
例如,您可能有一个自定义的record
具有多个属性,您的 bean 将其作为构造函数参数。
虽然这在常规运行时中运行良好,但 AOT 不知道如何生成自定义数据结构的代码。 一个好的经验法则是记住 bean 定义是多个模型之上的抽象。 建议分解为简单类型或引用以这种方式构建的 bean,而不是使用此类结构。
作为最后的手段,您可以实施自己的org.springframework.aot.generate.ValueCodeGenerator$Delegate
.
要使用它,请在META-INF/spring/aot.factories
使用Delegate
作为键。
避免使用自定义参数创建 Bean
Spring AOT 检测创建 bean 需要做什么,并使用实例供应商将其转换为生成的代码。 该容器还支持创建具有自定义参数的 bean,这会导致 AOT 出现几个问题:
-
自定义参数需要对匹配的构造函数或工厂方法进行动态内省。 AOT 无法检测到这些参数,因此必须手动提供必要的反射提示。
-
绕过实例供应商意味着创建后的所有其他优化也被跳过。 例如,字段和方法的自动装配将在实例供应商中处理时被跳过。
与其使用自定义参数创建原型范围的 bean,不如推荐使用手动工厂模式,其中 bean 负责创建实例。
避免循环依赖关系
某些用例可能会导致一个或多个 bean 之间出现循环依赖关系。使用
常规运行时,可以通过@Autowired
在 setter 方法或字段上。但是,AOT 优化的上下文将无法以
显式循环依赖关系。
因此,在 AOT 优化的应用程序中,您应该努力避免循环
依赖。如果无法做到这一点,您可以使用@Lazy
注射点或ObjectProvider
以延迟访问或检索必要的协作 bean。有关更多信息,请参阅此提示。
工厂豆
FactoryBean
应谨慎使用,因为它在 bean 类型解析方面引入了一个中间层,这在概念上可能并不必要。
根据经验,如果FactoryBean
实例不保持长期状态,并且在运行时的后续时间点不需要,则应将其替换为常规的工厂方法,可能使用FactoryBean
adapter 层(用于声明性配置目的)。
如果您的FactoryBean
implementation 不会解析对象类型(即T
),则需要格外小心。
请考虑以下示例:
-
Java
public class ClientFactoryBean<T extends AbstractClient> implements FactoryBean<T> {
// ...
}
具体的 client 声明应为 client 提供解析的泛型,如以下示例所示:
-
Java
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {
@Bean
public ClientFactoryBean<MyClient> myClient() {
return new ClientFactoryBean<>(...);
}
}
如果FactoryBean
bean 定义以编程方式注册,请确保按照以下步骤作:
-
用
RootBeanDefinition
. -
将
beanClass
到FactoryBean
类,以便 AOT 知道它是一个中间层。 -
将
ResolvableType
更改为 resolved generic 中,这可确保公开最精确的类型。
以下示例展示了一个基本定义:
-
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;
}
为了确保提前进行扫描,请PersistenceManagedTypes
bean 必须由
Factory bean 定义,如以下示例所示:
-
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 需要提前知道组件是否使用反射。 同样,除非明确指定,否则 Classpath 资源不会包含在本机映像中。 因此,如果应用程序需要加载资源,则必须从相应的 GraalVM 原生映像配置文件中引用该资源。
这RuntimeHints
API 在运行时收集对反射、资源加载、序列化和 JDK 代理的需求。
以下示例确保config/app.properties
可以在运行时从本机映像中的 Classpath 加载:
-
Java
runtimeHints.resources().registerPattern("config/app.properties");
在 AOT 处理期间,会自动处理许多合同。
例如,一个@Controller
方法,如果 Spring 检测到该类型应该被序列化(通常为 JSON),则会添加相关的反射提示。
对于核心容器无法推断的情况,您可以以编程方式注册此类提示。 还为常见使用案例提供了许多方便的注释。
@ImportRuntimeHints
RuntimeHintsRegistrar
实现允许您获取对RuntimeHints
由 AOT 引擎管理的实例。
此接口的实现可以使用@ImportRuntimeHints
在任何 Spring bean 上或@Bean
Factory 方法。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/*");
}
}
}
If at all possible, @ImportRuntimeHints
should be used as close as possible to the component that requires the hints.
This way, if the component is not contributed to the BeanFactory
, the hints won’t be contributed either.
It is also possible to register an implementation statically by adding an entry in META-INF/spring/aot.factories
with a key equal to the fully-qualified name of the RuntimeHintsRegistrar
interface.
@Reflective
@Reflective
provides an idiomatic way to flag the need for reflection on an annotated element.
For instance, @EventListener
is meta-annotated with @Reflective
since the underlying implementation invokes the annotated method using reflection.
By default, only Spring beans are considered, and an invocation hint is registered for the annotated element.
This can be tuned by specifying a custom ReflectiveProcessor
implementation via the
@Reflective
annotation.
Library authors can reuse this annotation for their own purposes.
If components other than Spring beans need to be processed, a BeanFactoryInitializationAotProcessor
can detect the relevant types and use ReflectiveRuntimeHintsRegistrar
to process them.
@RegisterReflectionForBinding
@RegisterReflectionForBinding
is a specialization of @Reflective
that registers the need for serializing arbitrary types.
A typical use case is the use of DTOs that the container cannot infer, such as using a web client within a method body.
@RegisterReflectionForBinding
can be applied to any Spring bean at the class level, but it can also be applied directly to a method, field, or constructor to better indicate where the hints are actually required.
The following example registers Account
for serialization.
-
Java
@Component
public class OrderService {
@RegisterReflectionForBinding(Account.class)
public void process(Order order) {
// ...
}
}
Testing Runtime Hints
Spring Core also ships RuntimeHintsPredicates
, a utility for checking that existing hints match a particular use case.
This can be used in your own tests to validate that a RuntimeHintsRegistrar
contains the expected results.
We can write a test for our SpellCheckService
and ensure that we will be able to load a dictionary at runtime:
@Test
void shouldRegisterResourceHints() {
RuntimeHints hints = new RuntimeHints();
new SpellCheckServiceRuntimeHints().registerHints(hints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.resource().forResource("dicts/en.txt"))
.accepts(hints);
}
With RuntimeHintsPredicates
, we can check for reflection, resource, serialization, or proxy generation hints.
This approach works well for unit tests but implies that the runtime behavior of a component is well known.
You can learn more about the global runtime behavior of an application by running its test suite (or the app itself) with the GraalVM tracing agent.
This agent will record all relevant calls requiring GraalVM hints at runtime and write them out as JSON configuration files.
For more targeted discovery and testing, Spring Framework ships a dedicated module with core AOT testing utilities, "org.springframework:spring-core-test"
.
This module contains the RuntimeHints Agent, a Java agent that records all method invocations that are related to runtime hints and helps you to assert that a given RuntimeHints
instance covers all recorded invocations.
Let’s consider a piece of infrastructure for which we’d like to test the hints we’re contributing during the AOT processing phase.
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);
}
}
}
We can then write a unit test (no native compilation required) that checks our contributed hints:
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);
}
}
If you forgot to contribute a hint, the test will fail and provide some details about the invocation:
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
There are various ways to configure this Java agent in your build, so please refer to the documentation of your build tool and test execution plugin.
The agent itself can be configured to instrument specific packages (by default, only org.springframework
is instrumented).
You’ll find more details in the Spring Framework buildSrc
README file.