Spring Framework:提前优化(Ahead of Time Optimizations)

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定义进行后处理,以提取额外的设置,如initdestroy方法。
  • 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();
	}

}

由于这个类没有任何特定条件,dataSourceConfigurationdataSource被识别为候选者。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被插桩)。

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值