Ahead of Time Optimizations

这一章涵盖了 Infra Ahead of Time (AOT) 优化。

对于集成测试特定的 AOT 支持, 参见 AOT Support for Tests.

AOT 介绍

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

提前应用此类优化意味着以下限制:

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

  • 在你的应用程序中定义的bean在运行时不能更改,意味着:

    • 特别是 @Profile 尤其是特定配置文件的配置,需要在构建时选择,并且当启用 AOT 时会在运行时自动启用。

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

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

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

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

也请参阅 最佳实践 部分.

当存在这些限制时,可以在构建时执行预处理,并生成额外的资产。一个经过 AOT 处理的基础设施应用程序通常会生成:

  • Java 源代码

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

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

目前,AOT 侧重于允许基础设施应用程序使用 GraalVM 部署为本机镜像。我们打算在未来的版本中支持更多基于 JVM 的用例。

AOT 引擎概述

AOT 引擎处理 ApplicationContext 的入口点是 ApplicationContextAotGenerator。 它负责以下步骤,基于表示要优化的应用程序的 GenericApplicationContextGenerationContext:

  • 对于 AOT 处理,刷新 ApplicationContext。与传统的刷新相反,这个版本只创建 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 {
}

class DataSourceConfiguration {
}

class ContainerConfiguration {
}

Starting this application with the regular runtime involves a number of steps including classpath scanning, configuration class parsing, bean instantiation, and lifecycle callback handling. Refresh for AOT processing only applies a subset of what happens with a regular refresh. AOT processing can be triggered as follows:

使用常规运行时启动此应用程序涉及许多步骤,包括类路径扫描、配置类解析、Bean 实例化和生命周期回调处理。 AOT 处理的刷新仅适用于 常规 refresh 所发生的部分。 可以通过以下方式触发 AOT 处理:

public void createAotContext() {
  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 实例,所以除了与 AOT 处理相关的特定变体之外,不会调用 BeanPostProcessor 实现。 这些特定的变体包括:

  • MergedBeanDefinitionPostProcessor 实现会后处理 Bean 定义,以提取额外的设置,例如 initdestroy 方法。

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

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

Bean 工厂初始化的 AOT Contributions

想要参与这一步骤的组件可以实现 BeanFactoryInitializationAotProcessor 接口。 每个实现都可以根据 bean 工厂的状态返回一个 AOT contribution。

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

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

BeanFactoryInitializationAotProcessor 接口也可以直接由一个 bean 实现。 在这种模式下,该 bean 提供的 AOT 贡献等同于它在常规运行时提供的功能。 因此,这样的 bean 会自动被排除在 AOT 优化的上下文之外。

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

Bean Registration AOT Contributions

一个核心的 BeanFactoryInitializationAotProcessor 实现负责收集每个候选 BeanDefinition 的必要贡献。 它使用专门的 BeanRegistrationAotProcessor 来实现这一点。 这个接口的使用如下:

  • 由一个 BeanPostProcessor bean 实现,以替换它的运行时行为。例如, AutowiredAnnotationBeanPostProcessor 实现了这个接口,以生成代码,注入带有 @Autowired 注解的成员。

  • 由一个在 META-INF/config/aot.factories 中注册的类型实现,其键等于接口的完全限定名。通常在需要调整 bean 定义以适应核心框架特定特性时使用。

如果一个 bean 实现了 BeanRegistrationAotProcessor 接口,那么该 bean 和 所有 它的依赖项都将在 AOT 处理期间初始化。 我们通常建议只有基础设施 bean,如 BeanFactoryPostProcessor,才实现这个接口,这些 bean 的依赖性有限,并且在 bean 工厂的生命周期早期已经初始化。 如果使用 @Bean 工厂方法注册这样的 bean,请确保该方法是 static 的,以便它的封闭 @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 定义的具体特性而有所不同。
每个生成的类都会用 cn.taketoday.aot.generate.Generated 进行注解,以便在需要排除它们时进行识别,例如通过静态分析工具。

以上生成的代码创建了等效于 @Configuration 类的bean定义,但以直接方式进行,尽可能地避免使用反射。 有一个用于 dataSourceConfiguration 的 bean 定义,以及一个用于 dataSourceBean 的 bean 定义。 当需要一个 datasource 实例时,会调用一个 BeanInstanceSupplier。 这个供应商会在 dataSourceConfiguration bean 上调用 dataSource() 方法。

使用 AOT 优化运行

AOT 是将 Infra 应用程序转换为本机可执行文件的必需步骤,因此在此模式下运行时会自动启用它。 可以通过将 infra.aot.enabled 系统属性设置为 true 来在 JVM 上使用这些优化。

当包含 AOT 优化时,一些在构建时做出的决策会硬编码在应用程序设置中。 例如,构建时启用的配置文件也会在运行时自动启用。

最佳实践

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 方法的返回类型尽可能精确。 考虑以下示例:

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

  @Bean
  public MyInterface myInterface() {
    return new MyImplementation();
  }

}

在上面的示例中,对于 myInterface bean,声明的类型是 MyInterface。 通常的后处理不会考虑 MyImplementation。 例如,如果 MyImplementation 上有一个带注解的处理方法,上下文应该注册它,那么它不会被预先检测到。

上面的示例应该重写如下:

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

  @Bean
  public MyImplementation myInterface() {
    return new MyImplementation();
  }

}

如果您正在以编程方式注册 bean 定义,请考虑使用 RootBeanDefinition,因为它允许指定一个处理泛型的 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 定义是以编程式注册的,请确保按照以下步骤进行操作:

  1. 使用 RootBeanDefinition

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

  3. ResolvableType 设置为一个解析的泛型,以确保暴露最精确的类型。

以下示例展示了一个基本的定义:

RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class);
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class));
// ...
registry.registerBeanDefinition("myClient", beanDefinition);

JPA

5.0 将不在支持 JPA

对于某些优化要应用,必须预先知道 JPA 持久化单元。 考虑以下基本示例:

@Bean
LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource) {
  LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
  factoryBean.setDataSource(dataSource);
  factoryBean.setPackagesToScan("com.example.app");
  return factoryBean;
}

为了确保提前进行扫描,必须声明一个 PersistenceManagedTypes bean,并由工厂 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;
}

Runtime Hints

在将应用程序作为本机镜像运行时,与常规 JVM 运行时相比,需要额外的信息。 例如,GraalVM 需要提前知道组件是否使用了反射。 同样,除非明确指定,否则类路径资源不会包含在本机镜像中。 因此,如果应用程序需要加载资源,则必须从相应的 GraalVM 本机镜像配置文件中引用。

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

runtimeHints.resources().registerPattern("config/app.properties");

在 AOT 处理期间,许多合同都会自动处理。 例如,会检查 @Controller 方法的返回类型,如果基础设施检测到应该对该类型进行序列化(通常为 JSON),则会添加相关的反射提示。

对于核心容器无法推断的情况,您可以以编程方式注册此类提示。 还提供了一些方便的注解,用于常见用例。

@ImportRuntimeHints

RuntimeHintsRegistrar 实现允许您获取由 AOT 引擎管理的 RuntimeHints 实例的回调。 可以使用 @ImportRuntimeHints 注解在任何 Infra bean 或 @Bean 工厂方法上注册此接口的实现。 RuntimeHintsRegistrar 实现在构建时被检测并调用。

@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/config/aot.factories 中添加一个键等于 RuntimeHintsRegistrar 接口的完全限定名称的条目来静态注册一个实现。

@Reflective

@Reflective 提供了一种标记对注释元素进行反射的成语化方式。 例如,@EventListener 被元注释为 @Reflective,因为底层实现会使用反射调用注释方法。

默认情况下,只考虑 Infra bean,并为注释元素注册调用提示。 可以通过指定自定义的 ReflectiveProcessor 实现来调整此设置,通过 @Reflective 注解。

库作者可以为自己的目的重用此注解。 如果除 Infra bean 外的组件需要处理,则 BeanFactoryInitializationAotProcessor 可以检测相关类型并使用 ReflectiveRuntimeHintsRegistrar 对其进行处理。

@RegisterReflectionForBinding

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

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

@Component
public class OrderService {

  @RegisterReflectionForBinding(Account.class)
  public void process(Order order) {
    // ...
  }

}

Testing Runtime Hints

Infra Core 还提供了 RuntimeHintsPredicates,这是一个用于检查现有提示是否与特定用例匹配的实用工具。 您可以在自己的测试中使用它来验证 RuntimeHintsRegistrar 包含了预期的结果。 我们可以为我们的 SpellCheckService 编写一个测试,并确保我们能够在运行时加载字典:

class SpellCheckServiceTests {

  @Test
  void shouldRegisterResourceHints() {
    RuntimeHints hints = new RuntimeHints();
    new SpellCheckServiceRuntimeHints().registerHints(hints, getClass().getClassLoader());
    assertThat(RuntimeHintsPredicates.resource().forResource("dicts/en.txt"))
            .accepts(hints);
  }

  // Copied here because it is package private in SpellCheckService
  static class SpellCheckServiceRuntimeHints implements RuntimeHintsRegistrar {

    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
      hints.resources().registerPattern("dicts/*");
    }
  }

}

使用 RuntimeHintsPredicates,我们可以检查反射、资源、序列化或代理生成提示。 这种方法非常适合单元测试,但前提是组件的运行时行为是众所周知的。

您可以通过使用 GraalVM tracing agent 在运行测试套件(或应用程序本身)时来了解应用程序的全局运行时行为。 该代理将记录在运行时需要 GraalVM 提示的所有相关调用,并将它们写出为 JSON 配置文件。

为了更有针对性地发现和测试,Infra Framework 提供了一个专用模块,核心 AOT 测试工具 "cn.taketoday:today-core-test"。 该模块包含 RuntimeHints Agent,它是一个 Java 代理,记录了所有与运行时提示相关的方法调用,并帮助您断言给定的 RuntimeHints 实例是否覆盖了所有记录的调用。 让我们考虑一个基础设施的例子,我们想要在 AOT 处理阶段测试我们正在贡献的提示。

public class SampleReflection {

  private final Logger logger = LoggerFactory.getLogger(SampleReflection.class);

  public void performReflection() {
    try {
      Class<?> version = ClassUtils.forName("cn.taketoday.lang.Version", null);
      Method getVersion = ReflectionUtils.getMethod(version, "get");
      Object versionObj = getVersion.invoke(null);
      logger.info("Infra version:" + versionObj);
    }
    catch (Exception exc) {
      logger.error("reflection failed", exc);
    }
  }

}

然后,我们可以编写一个单元测试(不需要本地编译),来检查我们贡献的提示:

// @EnabledIfRuntimeHintsAgent 表示标记了注释的测试类或测试方法仅在当前
// JVM 上加载 RuntimeHintsAgent 时才启用。
// 它还将测试标记为 "RuntimeHints" JUnit 标签。
@EnabledIfRuntimeHintsAgent
class SampleReflectionRuntimeHintsTests {

  @Test
  void shouldRegisterReflectionHints() {
    RuntimeHints runtimeHints = new RuntimeHints();
    // Call a RuntimeHintsRegistrar that contributes hints like:
    runtimeHints.reflection().registerType(Version.class, typeHint ->
            typeHint.withMethod("getVersion", List.of(), ExecutableMode.INVOKE));

    // 在记录 Lambda 中调用我们想要测试的相关代码片段。
    RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
      SampleReflection sample = new SampleReflection();
      sample.performReflection();
    });
    // 使用断言来验证记录的调用是否被贡献的提示所覆盖。
    assertThat(invocations).match(runtimeHints);
  }

}

如果忘记贡献提示,测试将失败,并提供有关调用的一些详细信息。

cn.taketoday.docs.core.aot.hints.testing.SampleReflection performReflection
INFO: Infra version:6.0.0-SNAPSHOT

Missing <"ReflectionHints"> for invocation <java.lang.Class#forName>
with arguments ["cn.taketoday.lang.Version",
    false,
    jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7].
Stacktrace:
<"cn.taketoday.util.ClassUtils#forName, Line 284
cn.taketoday.runtimehintstesting.SampleReflection#performReflection, Line 19
cn.taketoday.runtimehintstesting.SampleReflectionRuntimeHintsTests#lambda$shouldRegisterReflectionHints$0, Line 25

您可以通过各种方式配置此 Java 代理程序在构建中的使用,请参阅您的构建工具和测试执行插件的文档。 代理程序本身可以配置为仪器化特定的包(默认情况下仅仪器化`cn.taketoday`)。 您将在 Framework buildSrc README 文件中找到更多详细信息。