|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
预先优化
本章介绍 Spring 的预先编译(AOT)优化。
有关集成测试的 AOT 支持,请参阅测试的提前编译(Ahead of Time)支持。
AOT(提前)优化简介
Spring 对 AOT(提前编译)优化的支持旨在在构建时检查 ApplicationContext,并应用通常在运行时才执行的决策和发现逻辑。
这样做可以构建一个更直接、更聚焦的应用启动配置,该配置主要基于类路径和 Environment,并针对一组固定的功能进行优化。
过早应用此类优化意味着以下限制:
-
类路径在构建时即已固定并完全定义。
-
您应用程序中定义的 Bean 在运行时无法更改,这意味着:
-
@Profile,特别是特定于配置文件的配置需要在构建时进行选择。 -
影响 Bean 存在性的
Environment属性(@Conditional)仅在构建时被考虑。
-
-
使用实例供应器(lambda 表达式或方法引用)的 Bean 定义无法被提前转换(参见相关 spring-framework#29555 问题)。
-
确保 Bean 类型尽可能精确。
| 另请参阅最佳实践部分。 |
当这些限制生效时,便可以在构建时执行预先处理(ahead-of-time processing)并生成额外的资源文件。 经过 Spring AOT 处理的应用程序通常会生成:
-
Java 源代码
-
字节码(通常用于动态代理)
-
RuntimeHints用于反射、资源加载、序列化和 JDK 代理。
| 目前,AOT 专注于允许使用 GraalVM 将 Spring 应用程序部署为原生镜像。 我们计划在未来版本中支持更多基于 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 处理执行的刷新操作仅应用了 常规 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 实例,因此不会调用 BeanPostProcessor 的实现,除非是与 AOT 处理相关的特定变体。
这些变体包括:
-
MergedBeanDefinitionPostProcessor的实现类会对 bean 定义进行后处理,以提取额外的设置,例如init和destroy方法。 -
SmartInstantiationAwareBeanPostProcessor的实现会在必要时确定更精确的 bean 类型。 这确保了在运行时创建所需的任何代理。
一旦此阶段完成,BeanFactory 就包含了应用程序运行所需的 bean 定义。它不会触发 bean 的实例化,但允许 AOT 引擎检查将在运行时创建的 bean。
Bean Factory 初始化 AOT 贡献
希望参与此步骤的组件可以实现 BeanFactoryInitializationAotProcessor 接口。
每个实现都可以根据 Bean 工厂的状态返回一个 AOT 贡献。
AOT 贡献(AOT contribution)是一种组件,它贡献生成的代码以复现特定行为。
它还可以贡献 RuntimeHints,用于指明需要使用反射、资源加载、序列化或 JDK 代理。
可以在 BeanFactoryInitializationAotProcessor 中注册一个 META-INF/spring/aot.factories 实现,其键为该接口的全限定名。
BeanFactoryInitializationAotProcessor 也可以由一个 bean 直接实现。
在此模式下,该 bean 提供的 AOT 贡献与其在常规运行时所提供的功能等效。
因此,此类 bean 会自动从 AOT 优化的上下文中排除。
|
如果一个 Bean 实现了 |
Bean 注册的 AOT 贡献
一个核心的 BeanFactoryInitializationAotProcessor 实现负责收集每个候选 BeanDefinition 所需的贡献内容。
它通过一个专用的 BeanRegistrationAotProcessor 来实现这一点。
该接口的使用方式如下:
-
由一个
BeanPostProcessorBean 实现,用于替换其运行时行为。 例如,AutowiredAnnotationBeanPostProcessor实现了此接口,以生成注入带有@Autowired注解的成员的代码。 -
由在
META-INF/spring/aot.factories中注册的类型实现,其键等于该接口的全限定名。 通常在需要针对核心框架的特定功能调整 bean 定义时使用。
|
如果一个 Bean 实现了 |
如果没有 BeanRegistrationAotProcessor 处理某个已注册的 bean,则会由一个默认实现来处理它。
这是默认行为,因为针对 bean 定义调整生成的代码应仅限于特殊情况。
以我们之前的示例为例,假设 DataSourceConfiguration 如下所示:
-
Java
@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {
@Bean
public SimpleDataSource dataSource() {
return new SimpleDataSource();
}
}
由于该类没有任何特定条件,dataSourceConfiguration 和 dataSource 被识别为候选对象。
AOT 引擎会将上述配置类转换为类似于以下的代码:
-
Java
/**
* Bean definitions for {@link DataSourceConfiguration}
*/
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 定义的具体情况而有所不同。 |
上面生成的代码创建了与 @Configuration 类等效的 bean 定义,但采用了一种直接的方式,并且在可能的情况下不使用反射。
其中包含一个 dataSourceConfiguration 的 bean 定义和一个 dataSourceBean 的 bean 定义。
当需要一个 datasource 实例时,会调用一个 BeanInstanceSupplier。
该 supplier 会在 dataSource() bean 上调用 dataSourceConfiguration 方法。
最佳实践
AOT 引擎旨在处理尽可能多的使用场景,而无需对应用程序代码进行任何更改。 然而,请注意,某些优化是在构建时基于 Bean 的静态定义进行的。
本节列出了确保您的应用程序已准备好支持AOT的最佳实践。
暴露最精确的 Bean 类型
虽然您的应用程序可能会与某个 Bean 所实现的接口进行交互,但声明最精确的类型仍然非常重要。
AOT 引擎会对 Bean 类型执行额外的检查,例如检测是否存在 @Autowired 成员或生命周期回调方法。
对于 @Configuration 类,请确保工厂 @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。
工厂 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<>(...);
}
}
如果以编程方式注册了 FactoryBean 的 bean 定义,请确保遵循以下步骤:
-
使用
RootBeanDefinition。 -
将
beanClass设置为FactoryBean类,以便 AOT 能够识别它是一个中间层。 -
将
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;
}
为确保扫描提前进行,必须声明一个 PersistenceManagedTypes bean,并在工厂 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 运行时相比,以原生镜像(native image)方式运行应用程序需要额外的信息。 例如,GraalVM 需要提前知道某个组件是否使用了反射。 同样地,除非显式指定,否则类路径资源不会包含在原生镜像中。 因此,如果应用程序需要加载某个资源,则必须在相应的 GraalVM 原生镜像配置文件中引用该资源。
RuntimeHints API 在运行时收集了对反射、资源加载、序列化和 JDK 动态代理的需求。
以下示例确保 config/app.properties 可以在原生镜像中于运行时从类路径加载:
-
Java
runtimeHints.resources().registerPattern("config/app.properties");
在AOT处理过程中,许多契约会自动处理。
例如,@Controller方法的返回类型会被检查,如果Spring检测到该类型需要被序列化(通常为JSON格式),就会自动添加相关的反射提示。
对于核心容器无法推断的情况,你可以通过编程方式注册此类提示。 同时也提供了一些便捷的注解,用于常见的使用场景。
@ImportRuntimeHints
RuntimeHintsRegistrar 的实现允许你获得一个回调,以访问由 AOT 引擎管理的 RuntimeHints 实例。
该接口的实现可以通过在任意 Spring Bean 或 @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 应尽可能靠近需要这些提示(hints)的组件使用。
这样,如果该组件未被注册到 BeanFactory 中,相应的提示也不会被注册。
也可以通过在 META-INF/spring/aot.factories 文件中添加一个条目来静态注册实现,该条目的键为 RuntimeHintsRegistrar 接口的全限定名。
@Reflective
@Reflective 提供了一种符合习惯的方式,用于标记需要对注解元素进行反射。
例如,@EventListener 被 @Reflective 元注解,因为其底层实现通过反射调用被注解的方法。
默认情况下,仅考虑 Spring Bean,并为带注解的元素注册一个调用提示(invocation hint)。
这可以通过在 ReflectiveProcessor 注解中指定自定义的 @Reflective 实现来进行调整。
库的作者可以出于自己的目的重用此注解。
如果需要处理除 Spring Bean 之外的其他组件,BeanFactoryInitializationAotProcessor 可以检测相关类型,并使用 ReflectiveRuntimeHintsRegistrar 对其进行处理。
@RegisterReflectionForBinding
@RegisterReflectionForBinding 是 @Reflective 的一个特化,用于注册对任意类型进行序列化的需求。
典型用例是使用容器无法推断的 DTO(数据传输对象),例如在方法体中使用 Web 客户端。
@RegisterReflectionForBinding 可以在类级别应用于任何 Spring Bean,也可以直接应用于方法、字段或构造函数,以更明确地指示实际需要提示(hints)的位置。
以下示例将 Account 注册用于序列化。
-
Java
@Component
public class OrderService {
@RegisterReflectionForBinding(Account.class)
public void process(Order order) {
// ...
}
}
测试运行时提示
Spring Core 还提供了 RuntimeHintsPredicates,这是一个用于检查现有提示(hints)是否匹配特定使用场景的工具类。
你可以在自己的测试中使用它来验证 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);
}
}
如果你忘记提供一个提示(hint),测试将会失败,并提供有关该调用的一些详细信息:
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 进行插桩)。
您可以在 Spring Framework buildSrc README 文件中找到更多详细信息。