|
对于最新的稳定版本,请使用 Spring Framework 7.0.6! |
预先优化
本章涵盖Spring的提前编译(AOT)优化。
有关集成测试的特定AOT支持,请参阅测试的AOT支持。
提前优化介绍
Spring对AOT优化的支持旨在在构建时检查ApplicationContext,并应用通常在运行时执行的决策和发现逻辑。
这样做就能构建一个更直接的应用启动方案,该方案主要基于类路径和Environment,专注于固定的功能集。
提前应用此类优化意味着以下限制条件:
-
类路径在构建时是固定且完全定义的。
-
您的应用程序中定义的 Bean 在运行时无法更改,这意味着:
-
@Profile, 尤其需要在构建时选择特定于配置文件的配置。 -
Environment个影响 Bean 存在性的属性(@Conditional)仅在构建时被考虑。
-
-
使用实例提供者(lambda表达式或方法引用)定义的Bean无法进行提前转换(参见相关spring-framework#29555问题)。
-
确保 Bean 类型尽可能精确。
| 另请参阅最佳实践部分。 |
当这些限制存在时,便可在构建时执行提前处理并生成额外的资源文件。 经过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处理的刷新操作仅应用了常规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工厂初始化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来完成这一过程。
该接口的使用方式如下:
-
由
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。
此提供商会调用dataSourceConfiguration bean上的dataSource()方法。
最佳实践
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;
}
为确保扫描操作能够提前进行,工厂bean定义必须声明并使用一个PersistenceManagedTypes 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 需要预先知晓某个组件是否使用了反射。 类似地,类路径资源在未明确指定的情况下不会包含在原生镜像中。 因此,若应用程序需要加载资源,必须从对应的 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应该尽可能靠近需要提示的组件使用。
这样,如果该组件没有被添加到BeanFactory中,那么提示也不会被添加。
也可以通过向 META-INF/spring/aot.factories 添加一个条目来静态注册实现,该条目的键值等于 RuntimeHintsRegistrar 接口的完全限定名。
@Reflective
@Reflective 提供了一种惯用方式,用于标识被注解元素需要反射处理的需求。
例如,@EventListener 被元注解标记为 @Reflective,因为底层实现会通过反射机制调用被注解的方法。
默认情况下,仅考虑 Spring Bean 并为被注解元素注册调用提示。
可通过在 @Reflective 注解中指定自定义的 ReflectiveProcessor 实现来调整此行为。
库作者可以复用此注解用于自己的目的。
如果非Spring bean的组件需要被处理,BeanFactoryInitializationAotProcessor可以检测相关类型并使用ReflectiveRuntimeHintsRegistrar来处理它们。
@RegisterReflectionForBinding
@RegisterReflectionForBinding 是 @Reflective 的特殊化实现,它注册了序列化任意类型的需求。
典型用例是使用容器无法推断的DTO(例如在方法体内使用Web客户端)。
@RegisterReflectionForBinding 可应用于类级别的任何 Spring bean,也可直接应用于方法、字段或构造函数,以更清晰地指示何处真正需要提示。
以下示例为序列化注册了 Account。
-
Java
@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框架提供了一个专用模块,其中包含核心的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.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)。
更多细节请参阅Spring Framework buildSrc README文件。