事前の最適化

この章では、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 と dataSourceBean にはそれぞれ Bean 定義が存在します。datasource インスタンスが必要な場合は、BeanInstanceSupplier が呼び出されます。このサプライヤーは、dataSourceConfiguration Bean の dataSource() メソッドを呼び出します。

AOT 最適化で実行する

AOT は Spring アプリケーションをネイティブ実行ファイルに変換するための必須ステップであるため、ネイティブイメージ内で実行する場合は自動的に有効になります。ただし、システムプロパティ spring.aot.enabled を true に設定することで、JVM 上で AOT 最適化を利用することもできます。

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

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

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

}
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

	@Bean
	fun myInterface(): MyInterface = MyImplementation()

}

上記の例では、myInterface Bean の宣言型は MyInterface です。AOT 処理中は、通常の後処理において MyImplementation は考慮されません。たとえば、コンテキストが登録すべき MyImplementation にアノテーション付きのハンドラーメソッドがある場合、AOT 処理中にそのメソッドは検出されません。

上記の例は次のように書き直す必要があります。

  • Java

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

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

}
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

	@Bean
	fun myInterface() = MyImplementation()

}

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

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

コンテナーは複数の候補に基づいて、最も適切なコンストラクターを選択できます。ただし、これに頼るのはベストプラクティスではありません。必要に応じて、優先コンストラクターに @Autowired フラグを設定することをお勧めします。

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

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

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

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

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

カスタム引数を持つ Bean の作成を避ける

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

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

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

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

循環依存を避ける

特定のユースケースでは、1 つ以上の Bean 間で循環依存関係が発生する可能性があります。通常のランタイムでは、@Autowired を介して setter メソッドまたはフィールドを介してこれらの循環依存関係を接続できる場合があります。ただし、AOT に最適化されたコンテキストは、明示的な循環依存関係で開始できません。

AOT に最適化されたアプリケーションでは、循環依存関係を回避するように努める必要があります。それが不可能な場合は、@Lazy インジェクションポイントまたは ObjectProvider を使用して、必要な連携 Bean に遅延アクセスまたは取得することができます。詳細については、このヒントを参照してください。

FactoryBean

FactoryBean は、Bean 型解決において概念的に不要な中間層を導入するため、注意して使用する必要があります。目安として、FactoryBean インスタンスが長期的な状態を保持せず、実行時に後続の時点で不要になった場合は、通常の @Bean ファクトリメソッドに置き換え、場合によってはその上に FactoryBean アダプター層(宣言的な設定のため)を配置する必要があります。

FactoryBean 実装がオブジェクト型 (つまり T) を解決しない場合は、特別な注意が必要です。次の例を考えてみましょう。

  • Java

  • Kotlin

public class ClientFactoryBean<T extends AbstractClient> implements FactoryBean<T> {
	// ...
}
class ClientFactoryBean<T : AbstractClient> : FactoryBean<T> {
	// ...
}

次の例に示すように、具体的なクライアント宣言では、クライアントの解決されたジェネリクスを提供する必要があります。

  • Java

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

	@Bean
	public ClientFactoryBean<MyClient> myClient() {
		return new ClientFactoryBean<>(...);
	}

}
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

	@Bean
	fun myClient() = ClientFactoryBean<MyClient>(...)

}

FactoryBean Bean 定義がプログラムによって登録されている場合は、必ず次の手順に従ってください。

  1. RootBeanDefinition を使用してください。

  2. beanClass を FactoryBean クラスに設定して、AOT が中間層であることを認識できるようにします。

  3. ResolvableType を解決されたジェネリクスに設定すると、最も正確な型が確実に公開されます。

次の例は、基本的な定義を示しています。

  • Java

  • Kotlin

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

JPA

特定の最適化を適用するには、JPA 永続ユニットを事前に知っておく必要があります。次の基本的な例を考えてみましょう。

  • Java

  • Kotlin

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

エンティティのスキャンが事前に行われるようにするには、次の例に示すように、PersistenceManagedTypes Bean を宣言してファクトリの Bean 定義で使用する必要があります。

  • Java

  • Kotlin

@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;
}
@Bean
fun persistenceManagedTypes(resourceLoader: ResourceLoader): PersistenceManagedTypes {
	return PersistenceManagedTypesScanner(resourceLoader)
			.scan("com.example.app")
}

@Bean
fun customDBEntityManagerFactory(dataSource: DataSource, managedTypes: PersistenceManagedTypes): LocalContainerEntityManagerFactoryBean {
	val factoryBean = LocalContainerEntityManagerFactoryBean()
	factoryBean.dataSource = dataSource
	factoryBean.setManagedTypes(managedTypes)
	return factoryBean
}

実行時のヒント

アプリケーションをネイティブイメージとして実行するには、通常の JVM ランタイムと比較して追加情報が必要です。たとえば、GraalVM は、コンポーネントがリフレクションを使用するかどうかを事前に知る必要があります。同様に、明示的に指定しない限り、クラスパスリソースはネイティブイメージに含まれません。アプリケーションがリソースをロードする必要がある場合は、対応する GraalVM ネイティブイメージ構成ファイルからリソースを参照する必要があります。

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

  • Java

  • Kotlin

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

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

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

@ImportRuntimeHints

RuntimeHintsRegistrar (Javadoc) 実装により、AOT エンジンによって管理される RuntimeHints インスタンスへのコールバックを取得できます。このインターフェースの実装は、Spring、Bean、@Bean のファクトリメソッドに @ImportRuntimeHints (Javadoc) を使用して登録できます。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 のみが考慮されますが、@ReflectiveScan (Javadoc) を使用してスキャンするようにオプトインすることもできます。以下の例では、com.example.app パッケージとそのサブパッケージ内のすべての型が考慮されます。

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ReflectiveScan;

@Configuration
@ReflectiveScan("com.example.app")
public class MyConfiguration {
}

スキャンは AOT 処理中に行われ、対象パッケージ内の型はクラスレベルのアノテーションがなくても考慮されます。これによりディープスキャンが実行され、型、フィールド、コンストラクター、メソッド、囲まれた要素において、@Reflective が直接またはメタアノテーションとして存在するかどうかがチェックされます。

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

ライブラリ作成者は、このアノテーションを独自の目的で再利用できます。このようなカスタマイズの例については、次のセクションで説明します。

@RegisterReflection

@RegisterReflection (Javadoc) は、任意の型のリフレクションを登録するための宣言的な方法を提供する @Reflective の特殊化です。

@Reflective の特殊化として、@ReflectiveScan を使用している場合は @RegisterReflection も検出されます。

次の例では、パブリックコンストラクターとパブリックメソッドは、AccountService のリフレクションを介して呼び出すことができます。

@Configuration
@RegisterReflection(classes = AccountService.class, memberCategories =
		{ MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS })
class MyConfiguration {
}

@RegisterReflection はクラスレベルの任意のターゲット型に適用できますが、ヒントが実際に必要な場所をより適切に示すためにメソッドに直接適用することもできます。

@RegisterReflection は、より具体的なニーズをサポートするためのメタアノテーションとして使用できます。@RegisterReflectionForBinding (Javadoc) は、@RegisterReflection でメタアノテーションされた複合アノテーションであり、任意の型の直列化の必要性を登録します。典型的なユースケースとしては、コンテナーが推論できない DTO の使用、たとえばメソッド本体内での Web クライアントの使用などが挙げられます。

次の例では、直列化のために Order を登録します。

@Component
class OrderService {

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

}

これは、Order のコンストラクター、フィールド、プロパティ、レコードコンポーネントのヒントを登録します。ヒントは、プロパティとレコードコンポーネントで推移的に使用される型にも登録されます。つまり、Order が他の型を公開する場合、それらの型にもヒントが登録されます。

規約ベースの変換のための実行時ヒント

コアコンテナーには、多くの一般的な型の自動変換に対する組み込みサポートが用意されています (Spring 型変換を参照)。ただし、一部の変換は、リフレクションに依存する規則ベースのアルゴリズムによってサポートされています。

具体的には、特定のソース→ターゲット型のペアに対して ConversionService に明示的な Converter が登録されていない場合、内部 ObjectToObjectConverter は、ソースオブジェクトのメソッド、またはターゲット型の静的ファクトリメソッドもしくはコンストラクターに委譲することで、規約に基づいてソースオブジェクトをターゲット型に変換しようとします。この規約ベースのアルゴリズムは実行時に任意の型に適用できるため、コアコンテナーはこのようなリフレクションをサポートするために必要な実行時ヒントを推論できません。

ランタイムヒントの不足が原因でネイティブイメージ内で規則ベースの変換の問題が発生した場合は、必要なヒントをプログラムで登録できます。例: アプリケーションで java.time.Instant から java.sql.Timestamp への変換が必要で、リフレクションを使用して java.sql.Timestamp.from(Instant) を呼び出すために ObjectToObjectConverter に依存している場合は、次の例に示すように、ネイティブイメージ内でこのユースケースをサポートするカスタム RuntimeHintsRegitrar を実装できます。

  • Java

public class TimestampConversionRuntimeHints implements RuntimeHintsRegistrar {

	public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
		ReflectionHints reflectionHints = hints.reflection();

		reflectionHints.registerTypeIfPresent(classLoader, "java.sql.Timestamp", hint -> hint
				.withMethod("from", List.of(TypeReference.of(Instant.class)), ExecutableMode.INVOKE)
				.onReachableType(TypeReference.of("java.sql.Timestamp")));
	}
}

TimestampConversionRuntimeHints は、@ImportRuntimeHints を介して宣言的に登録することも、META-INF/spring/aot.factories 構成ファイルを介して静的に登録することもできます。

上記の TimestampConversionRuntimeHints クラスは、フレームワークに含まれており、デフォルトで登録されている ObjectToObjectConverterRuntimeHints クラスの簡略化されたバージョンです。

この特定の Instant から Timestamp へのユースケースは、フレームワークによってすでに処理されています。

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

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

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] (英語) ファイルを参照してください。