事前の最適化

この章では、Spring の Ahead of Time (AOT) 最適化について説明します。

統合テストに固有の AOT サポートについては、テストの事前サポートを参照してください。

事前最適化の概要

AOT 最適化に対する Spring のサポートは、ビルド時に ApplicationContext をインスペクションし、実行時に通常発生する決定と検出ロジックを適用することを目的としています。そうすることで、より簡単で、主にクラスパスと Environment に基づいた固定の機能セットに焦点を当てたアプリケーション起動の配置を構築できます。

このような最適化を早期に適用すると、次の制限が発生します。

  • クラスパスは固定され、ビルド時に完全に定義されます。

  • アプリケーションで定義された Bean は実行時に変更できません。つまり、次のことを意味します。

    • @Profile、特にプロファイル固有の構成はビルド時に選択する必要があり、AOT が有効になっている場合は実行時に自動的に有効になります。

    • Bean (@Conditional) の存在に影響を与える Environment プロパティは、ビルド時にのみ考慮されます。

  • インスタンスサプライヤー (ラムダまたはメソッド参照) を含む Bean 定義は、事前に変換できません。

  • シングルトンとして登録された Bean (通常は ConfigurableListableBeanFactory からの registerSingleton を使用) も、事前に変換することはできません。

  • インスタンスに依存することはできないため、Bean 型ができるだけ正確であることを確認してください。

ベストプラクティスセクションも参照してください。

これらの制限が適用されると、ビルド時に事前処理を実行し、追加のアセットを生成することが可能になります。Spring AOT 処理済みアプリケーションは、通常、次のものを生成します。

  • Java ソースコード

  • バイトコード (通常は動的プロキシ用)

  • RuntimeHints (Javadoc) (リフレクション、リソースの読み込み、直列化、JDK プロキシを使用する場合)

現在、AOT は、GraalVM を使用して Spring アプリケーションをネイティブイメージとしてデプロイできるようにすることに重点を置いています。将来の世代では、より多くの JVM ベースのユースケースをサポートする予定です。

AOT エンジンの概要

ApplicationContext を処理するための AOT エンジンのエントリポイントは ApplicationContextAotGenerator です。最適化するアプリケーションを表す GenericApplicationContext と GenerationContext (Javadoc) に基づいて、次の手順が実行されます。

  • AOT 処理のために ApplicationContext をリフレッシュします。従来のリフレッシュとは対照的に、このバージョンでは、Bean インスタンスではなく、Bean 定義のみが作成されます。

  • 利用可能な BeanFactoryInitializationAotProcessor 実装を呼び出し、それらのコントリビューションを GenerationContext に対して適用します。たとえば、コア実装はすべての候補 Bean 定義を繰り返し処理し、必要なコードを生成して BeanFactory の状態を復元します。

このプロセスが完了すると、GenerationContext は、アプリケーションの実行に必要な生成されたコード、リソース、クラスで更新されます。RuntimeHints インスタンスを使用して、関連する GraalVM ネイティブイメージ構成ファイルを生成することもできます。

ApplicationContextAotGenerator#processAheadOfTime は、AOT 最適化でコンテキストを開始できるようにする ApplicationContextInitializer エントリポイントのクラス名を返します。

これらの手順については、以下のセクションで詳しく説明します。

AOT 処理のリフレッシュ

AOT 処理のリフレッシュは、すべての GenericApplicationContext 実装でサポートされています。アプリケーションコンテキストは、通常は @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 の実装が通常どおり呼び出されます。これには、構成クラスの解析、インポートセレクター、クラスパスのスキャンなどが含まれます。このような手順により、アプリケーションに関連する Bean 定義が BeanRegistry に含まれていることを確認できます。Bean 定義が条件 ( @Profile など) で保護されている場合、これらが評価され、条件に一致しない Bean 定義はこの段階で破棄されます。

カスタムコードでプログラム的に追加の Bean を登録する必要がある場合は、Bean 定義のみが考慮されるため、カスタム登録コードで BeanFactory ではなく BeanDefinitionRegistry を使用するようにしてください。良いパターンは、ImportBeanDefinitionRegistrar を実装し、それを @Import 経由で構成クラスの 1 つに登録することです。

このモードは実際には Bean インスタンスを作成しないため、AOT 処理に関連する特定のバリアントを除いて、BeanPostProcessor 実装は呼び出されません。

  • MergedBeanDefinitionPostProcessor 実装は、Bean 定義を後処理して、init メソッドや destroy メソッドなどの追加設定を抽出します。

  • SmartInstantiationAwareBeanPostProcessor 実装は、必要に応じて、より正確な Bean 型を決定します。これにより、実行時に必要なプロキシが確実に作成されます。

このパートが完了すると、アプリケーションの実行に必要な Bean 定義が BeanFactory に含まれます。これは Bean インスタンス化をトリガーしませんが、AOT エンジンが実行時に作成される Bean をインスペクションできるようにします。

Bean ファクトリ初期化 AOT コントリビューション

このステップに参加するコンポーネントは、BeanFactoryInitializationAotProcessor (Javadoc) インターフェースを実装できます。各実装は、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 ファクトリメソッドを使用して登録されている場合は、メソッドが 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 ファクトリメソッドを使用して登録されている場合は、メソッドが static であることを確認して、それを囲む @Configuration クラスを初期化する必要がないようにしてください。

BeanRegistrationAotProcessor が特定の登録済み Bean を処理しない場合、デフォルトの実装がそれを処理します。Bean 定義用に生成されたコードのチューニングはコーナーケースに制限する必要があるため、これがデフォルトの動作です。

前の例で、DataSourceConfiguration が次のようになっているとします。

  • Java

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {

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

}
@Configuration(proxyBeanMethods = false)
class DataSourceConfiguration {

	@Bean
	fun dataSource() = SimpleDataSource()

}
無効な Java 識別子 (文字で始まっていない、スペースが含まれているなど) を使用するバッククォート付きの Kotlin クラス名はサポートされていません。

このクラスには特に条件がないため、dataSourceConfiguration と dataSource が候補として識別されます。AOT エンジンは、上記の構成クラスを次のようなコードに変換します。

  • Java

/**
 * 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 用の Bean 定義があります。datasource インスタンスが必要な場合は、BeanInstanceSupplier が呼び出されます。このサプライヤーは、dataSourceConfiguration Bean で dataSource() メソッドを呼び出します。

AOT 最適化で実行する

AOT は、Spring アプリケーションをネイティブ実行可能ファイルに変換するための必須の手順であるため、このモードで実行すると自動的に有効になります。spring.aot.enabled システムプロパティを true に設定することで、JVM でこれらの最適化を使用できます。

AOT 最適化が組み込まれている場合、ビルド時に行われた一部の決定は、アプリケーションセットアップでハードコードされます。たとえば、ビルド時に有効にされたプロファイルは、実行時にも自動的に有効になります。

ベストプラクティス

AOT エンジンは、アプリケーションのコードを変更することなく、できるだけ多くのユースケースを処理できるように設計されています。ただし、一部の最適化は Bean の静的定義に基づいてビルド時に行われることに注意してください。

このセクションでは、アプリケーションが AOT に対応できるようにするためのベストプラクティスを示します。

プログラムによる Bean 登録

AOT エンジンは、@Configuration モデルと、構成処理の一部として呼び出される可能性のあるコールバックを処理します。追加の Bean をプログラムで登録する必要がある場合は、必ず BeanDefinitionRegistry を使用して Bean 定義を登録してください。

これは通常、BeanDefinitionRegistryPostProcessor を介して実行できます。それ自体が Bean として登録されている場合、必ず BeanFactoryInitializationAotProcessor も実装しない限り、実行時に再度呼び出されることに注意してください。より慣用的な方法は、ImportBeanDefinitionRegistrar を実装し、構成クラスの 1 つで @Import を使用して登録することです。これにより、構成クラス解析の一部としてカスタムコードが呼び出されます。

別のコールバックを使用して追加の Bean をプログラムで宣言した場合、AOT エンジンによって処理されない可能性が高いため、それらに対するヒントは生成されません。環境によっては、これらの Bean がまったく登録されない場合があります。たとえば、ネイティブイメージではクラスパスの概念がないため、クラスパススキャンは機能しません。このような場合、ビルド時にスキャンが行われることが重要です。

最も高精度な Bean 型を公開

アプリケーションは Bean が実装するインターフェースと対話する可能性がありますが、最も正確な型を宣言することが依然として非常に重要です。AOT エンジンは、@Autowired メンバーやライフサイクルコールバックメソッドの存在の検出など、Bean 型に対して追加のチェックを実行します。

@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 定義をプログラムで登録している場合は、ジェネリクスを処理する ResolvableType を指定できる RootBeanBefinition の使用を検討してください。

複数のコンストラクターを避ける

コンテナーは、いくつかの候補に基づいて、使用する最も適切なコンストラクターを選択できます。ただし、これはベストプラクティスではないため、必要に応じて優先コンストラクターに @Autowired でフラグを付けることをお勧めします。

変更できないコードベースで作業している場合は、関連する Bean 定義に preferredConstructors 属性 (Javadoc) を設定して、どのコンストラクターを使用する必要があるかを示すことができます。

コンストラクターパラメーターとプロパティの複雑なデータ構造を避ける

RootBeanDefinition をプログラムで作成する場合、使用できる型に関して制約はありません。たとえば、Bean がコンストラクター引数として受け取るいくつかのプロパティを持つカスタム record がある場合があります。

これは通常のランタイムでは問題なく動作しますが、AOT はカスタムデータ構造のコードを生成する方法を知りません。Bean 定義は複数のモデルに基づく抽象化であることを覚えておくと良いでしょう。このような構造を使用するのではなく、単純な型に分解するか、そのように構築された Bean を参照することをお勧めします。

最後の手段として、独自の org.springframework.aot.generate.ValueCodeGenerator$Delegate を実装することができます。これを使用するには、Delegate をキーとして使用して、その完全修飾名を META-INF/spring/aot.factories に登録します。

カスタム引数で Bean を作成しないでください

Spring AOT は、Bean を作成するために何を行う必要があるかを検出し、インスタンスサプライヤーを使用してそれを生成されたコードに変換します。コンテナーは、カスタム引数 (Javadoc) を使用した Bean の作成もサポートしており、これにより AOT でいくつかの問題が発生します。

  1. カスタム引数には、一致するコンストラクターまたはファクトリメソッドの動的なイントロスペクションが必要です。これらの引数は AOT では検出できないため、必要なリフレクションヒントを手動で提供する必要があります。

  2. インスタンスサプライヤーをバイパスすると、作成後の他のすべての最適化もスキップされます。たとえば、フィールドとメソッドのオートワイヤーは、インスタンスサプライヤーで処理されるためスキップされます。

カスタム引数を使用してプロトタイプスコープの Bean を作成するのではなく、Bean がインスタンスの作成を担当する手動ファクトリパターンをお勧めします。

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 (Javadoc) API は、実行時のリフレクション、リソースのロード、直列化、JDK プロキシの必要性を収集します。次の例では、実行時にネイティブイメージ内のクラスパスから config/app.properties をロードできることを確認します。

  • Java

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

多くの契約は、AOT 処理中に自動的に処理されます。たとえば、@Controller メソッドの戻り値の型がインスペクションされ、型を (通常は JSON に) 直列化する必要があることが Spring によって検出された場合、関連するリフレクションヒントが追加されます。

コアコンテナーが推測できないケースについては、そのようなヒントをプログラムで登録できます。一般的なユースケースのために、多くの便利なアノテーションも提供されています。

@ImportRuntimeHints

RuntimeHintsRegistrar 実装により、AOT エンジンによって管理される RuntimeHints インスタンスへのコールバックを取得できます。このインターフェースの実装は、Spring Bean または @Bean ファクトリメソッドで @ImportRuntimeHints を使用して登録できます。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 に提供されない場合、ヒントも提供されません。

RuntimeHintsRegistrar インターフェースの完全修飾名と等しいキーを持つエントリを META-INF/spring/aot.factories に追加することで、実装を静的に登録することもできます。

@Reflective

@Reflective (Javadoc) は、アノテーション付き要素でリフレクションが必要であることを示す慣用的な方法を提供します。たとえば、@EventListener は @Reflective でメタアノテーションが付けられます。これは、基になる実装がリフレクションを使用してアノテーション付きメソッドを呼び出すためです。

デフォルトでは、Spring Bean のみが考慮され、アノテーション付き要素に対して呼び出しヒントが登録されます。これは、@Reflective アノテーションを介してカスタム ReflectiveProcessor 実装を指定することで調整できます。

ライブラリの作成者は、このアノテーションを独自の目的で再利用できます。Spring Bean 以外のコンポーネントを処理する必要がある場合、BeanFactoryInitializationAotProcessor は関連する型を検出し、ReflectiveRuntimeHintsRegistrar を使用して処理できます。

@RegisterReflectionForBinding

@RegisterReflectionForBinding (Javadoc) は、任意の型を直列化する必要性を登録する @Reflective の特殊化です。典型的な使用例は、メソッド本体内での Web クライアントの使用など、コンテナーが推測できない DTO の使用です。

@RegisterReflectionForBinding は、クラスレベルで任意の Spring Bean に適用できますが、ヒントが実際に必要な場所をより適切に示すために、メソッド、フィールド、コンストラクターに直接適用することもできます。次の例では、直列化のために Account を登録します。

  • Java

@Component
public class OrderService {

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

}

ランタイムヒントのテスト

Spring コアには、既存のヒントが特定のユースケースと一致するかどうかを確認するためのユーティリティである 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 インスタンスが記録されたすべての呼び出しをカバーしていることをアサートするのに役立つ 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 [GitHub] (英語) ファイルを参照してください。