预先优化

本章介绍了Spring的预先优化(AOT)。

有关集成测试特定的AOT支持,请参见测试的预先支持

预先优化简介

Spring对AOT优化的支持旨在在构建时检查ApplicationContext并应用通常在运行时发生的决策和发现逻辑。这样做可以构建一个更简单、更专注于基于类路径和Environment的固定功能集的应用程序启动安排。

提前应用这些优化意味着以下限制:

  • 类路径在构建时是固定且完全定义的。

  • 应用程序中定义的bean在运行时无法更改,这意味着:

    • @Profile,特别是需要在构建时选择特定配置文件。

    • 影响bean存在的Environment属性(@Conditional)仅在构建时考虑。

  • 具有实例提供程序(lambda或方法引用)的bean定义无法提前转换。

  • 作为单例注册的bean(使用registerSingleton,通常来自ConfigurableListableBeanFactory)也无法提前转换。

  • 由于我们无法依赖实例,请确保bean类型尽可能精确。

另请参阅最佳实践部分。

当存在这些限制时,就可以在构建时执行预先处理并生成额外的资产。Spring AOT处理的应用程序通常生成:

  • Java源代码

  • 字节码(通常用于动态代理)

  • RuntimeHints用于反射、资源加载、序列化和JDK代理的使用

目前,AOT专注于允许使用GraalVM将Spring应用程序部署为本机映像。我们打算在未来的版本中支持更多基于JVM的用例。

AOT引擎概述

处理ApplicationContext的AOT引擎的入口点是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定义。

如果自定义代码需要以编程方式注册额外的bean,请确保自定义注册代码使用BeanDefinitionRegistry而不是BeanFactory,因为只有bean定义会被考虑。一个好的模式是实现ImportBeanDefinitionRegistrar,并通过一个配置类的@Import来注册它。

因为此模式实际上不会创建bean实例,所以不会调用BeanPostProcessor实现,除非是与AOT处理相关的特定变体。这些包括:

  • MergedBeanDefinitionPostProcessor实现会后处理bean定义以提取附加设置,例如initdestroy方法。

  • SmartInstantiationAwareBeanPostProcessor实现会在必要时确定更精确的bean类型。这确保在运行时创建任何所需的代理。

完成此部分后,BeanFactory包含了应用程序运行所必需的bean定义。它不会触发bean实例化,但允许AOT引擎检查将在运行时创建的bean。

Bean Factory初始化AOT贡献

希望参与此步骤的组件可以实现BeanFactoryInitializationAotProcessor接口。每个实现可以根据bean工厂的状态返回一个AOT贡献。

AOT贡献是一个组件,它提供生成的代码,复制特定的行为。它还可以提供RuntimeHints,指示需要反射、资源加载、序列化或JDK代理。

BeanFactoryInitializationAotProcessor实现可以在META-INF/spring/aot.factories中注册,键值等于接口的完全限定名。

BeanFactoryInitializationAotProcessor接口也可以由bean直接实现。在这种模式下,bean提供与常规运行时提供的功能等效的AOT贡献。因此,这样的bean会自动从AOT优化的上下文中排除。

如果一个bean实现了BeanFactoryInitializationAotProcessor接口,则该bean及其所有依赖项将在AOT处理期间初始化。我们通常建议此接口仅由基础设施bean(如BeanFactoryPostProcessor)实现,这些bean具有有限的依赖关系,并且在bean工厂生命周期的早期已经初始化。如果使用@Bean工厂方法注册这样的bean,请确保该方法是static,以便其封闭的@Configuration类不必被初始化。

Bean注册AOT贡献

核心的BeanFactoryInitializationAotProcessor实现负责收集每个候选BeanDefinition所需的贡献。它使用专用的BeanRegistrationAotProcessor来实现。

此接口的使用方式如下:

  • 由一个BeanPostProcessor bean实现,以替换其运行时行为。例如,AutowiredAnnotationBeanPostProcessor实现此接口以生成代码,注入用@Autowired注释的成员。

  • 由在META-INF/spring/aot.factories中注册的类型实现,键值等于接口的完全限定名。通常在bean定义需要针对核心框架的特定特性进行调整时使用。

如果一个bean实现了BeanRegistrationAotProcessor接口,则该bean及其所有依赖项将在AOT处理期间初始化。我们通常建议此接口仅由基础设施bean(如BeanFactoryPostProcessor)实现,这些bean具有有限的依赖关系,并且在bean工厂生命周期的早期已经初始化。如果使用@Bean工厂方法注册这样的bean,请确保该方法是static,以便其封闭的@Configuration类不必被初始化。

如果没有BeanRegistrationAotProcessor处理特定注册的bean,将使用默认实现处理它。这是默认行为,因为将生成的代码调整为bean定义应该限制在极端情况下。

以前的示例为例,假设DataSourceConfiguration如下:

  • Java

@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {

	@Bean
	public SimpleDataSource dataSource() {
		return new SimpleDataSource();
	}

}

由于此类没有特定条件,dataSourceConfigurationdataSource被识别为候选项。AOT引擎将把上述配置类转换为类似以下代码的代码:

  • Java

/**
 * {@link DataSourceConfiguration}的bean定义
 */
@Generated
public class DataSourceConfiguration__BeanDefinitions {
	/**
	 * 获取'dataSourceConfiguration'的bean定义
	 */
	public static BeanDefinition getDataSourceConfigurationBeanDefinition() {
		Class<?> beanType = DataSourceConfiguration.class;
		RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
		beanDefinition.setInstanceSupplier(DataSourceConfiguration::new);
		return beanDefinition;
	}

	/**
	 * 获取'dataSource'的bean实例供应商
	 */
	private static BeanInstanceSupplier<SimpleDataSource> getDataSourceInstanceSupplier() {
		return BeanInstanceSupplier.<SimpleDataSource>forFactoryMethod(DataSourceConfiguration.class, "dataSource")
				.withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(DataSourceConfiguration.class).dataSource());
	}

	/**
	 * 获取'dataSource'的bean定义
	 */
	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的bean定义。当需要datasource实例时,将调用BeanInstanceSupplier。此供应商调用dataSourceConfiguration bean上的dataSource()方法。

最佳实践

AOT引擎旨在处理尽可能多的用例,而无需在应用程序中进行代码更改。但是,请记住,一些优化是在构建时基于bean的静态定义进行的。

本节列出了确保您的应用程序准备好进行AOT的最佳实践。

编程式注册Bean

AOT引擎负责处理@Configuration模型和可能作为配置处理的任何回调。如果需要以编程方式注册额外的Bean,请确保使用BeanDefinitionRegistry来注册Bean定义。

通常可以通过BeanDefinitionRegistryPostProcessor来实现。请注意,如果它本身注册为一个Bean,除非确保同时实现BeanFactoryInitializationAotProcessor,否则在运行时会再次调用它。更符合惯例的方法是实现ImportBeanDefinitionRegistrar,并在其中一个配置类上使用@Import进行注册。这会在配置类解析过程中调用您的自定义代码。

如果使用不同的回调声明额外的Bean,这些Bean可能不会被AOT引擎处理,因此不会为它们生成提示。根据环境的不同,这些Bean可能根本不会被注册。例如,在本地镜像中,类路径扫描不起作用,因为没有类路径的概念。对于这种情况,关键是扫描发生在构建时。

暴露最精确的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

避免多个构造函数

容器能够根据多个候选项选择最合适的构造函数。然而,这不是最佳实践,如果必要的话,最好使用@Autowired标记首选的构造函数。

如果您正在处理无法修改的代码库,可以在相关Bean定义上设置preferredConstructors属性,以指示应使用哪个构造函数。

FactoryBean

应谨慎使用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定义,请确保遵循以下步骤:

  1. 使用RootBeanDefinition

  2. beanClass设置为FactoryBean类,以便AOT知道它是一个中间层。

  3. 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运行时更多的信息。例如,GraalVM需要提前知道组件是否使用反射。同样,除非明确指定,否则类路径资源不会包含在本机映像中。因此,如果应用程序需要加载资源,则必须从相应的GraalVM本机映像配置文件中引用。

RuntimeHints API在运行时收集了对反射、资源加载、序列化和JDK代理的需求。以下示例确保config/app.properties可以在本机映像中的类路径下运行时加载:

  • Java

runtimeHints.resources().registerPattern("config/app.properties");
@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,因为底层实现使用反射调用注释方法。

@Reflective注释指定自定义 ReflectiveProcessor实现来进行调整。

BeanFactoryInitializationAotProcessor可以检测相关类型并使用 ReflectiveRuntimeHintsRegistrar来处理它们。

@RegisterReflectionForBinding

@RegisterReflectionForBinding@Reflective的一个特化,用于注册序列化任意类型的需求。一个典型的用例是容器无法推断的DTO的使用,例如在方法体内使用Web客户端。

@RegisterReflectionForBinding可以应用于类级别的任何Spring bean,但也可以直接应用于方法、字段或构造函数,以更好地指示提示实际上所需的位置。以下示例将Account注册为可序列化。

  • 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.