最新の安定バージョンについては、Spring Framework 6.2.8 を使用してください! |
事前の最適化
この章では、Spring の Ahead of Time (AOT) 最適化について説明します。
統合テストに固有の AOT サポートについては、テストの事前サポートを参照してください。
事前最適化の概要
AOT 最適化に対する Spring のサポートは、ビルド時に ApplicationContext
をインスペクションし、実行時に通常発生する決定と検出ロジックを適用することを目的としています。そうすることで、より簡単で、主にクラスパスと Environment
に基づいた固定の機能セットに焦点を当てたアプリケーション起動の配置を構築できます。
このような最適化を早期に適用すると、次の制限が発生します。
クラスパスは固定され、ビルド時に完全に定義されます。
アプリケーションで定義された Bean は実行時に変更できません。つまり、次のことを意味します。
@Profile
、特にプロファイル固有の構成はビルド時に選択する必要があります。Bean (
@Conditional
) の存在に影響を与えるEnvironment
プロパティは、ビルド時にのみ考慮されます。
インスタンスサプライヤー (ラムダまたはメソッド参照) を含む Bean 定義は、事前に変換できません (関連する spring-framework#29555 [GitHub] (英語) の課題を参照してください)。
Bean 型ができるだけ正確であることを確認してください。
ベストプラクティスセクションも参照してください。 |
これらの制限が適用されると、ビルド時に事前処理を実行し、追加のアセットを生成することが可能になります。Spring AOT 処理済みアプリケーションは、通常、次のものを生成します。
Java ソースコード
バイトコード (通常は動的プロキシ用)
リフレクション、リソースの読み込み、直列化、JDK プロキシを使用するための
RuntimeHints
(Javadoc) 。
現在、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 インスタンスを作成しないため、AOT 処理に関連する特定のバリアントを除いて、BeanPostProcessor
実装は呼び出されません。
MergedBeanDefinitionPostProcessor
実装は、Bean 定義を後処理して、init
メソッドやdestroy
メソッドなどの追加設定を抽出します。SmartInstantiationAwareBeanPostProcessor
実装は、必要に応じて、より正確な Bean 型を決定します。これにより、実行時に必要なプロキシが確実に作成されます。
このパートが完了すると、アプリケーションの実行に必要な Bean 定義が BeanFactory
に含まれます。これは Bean インスタンス化をトリガーしませんが、AOT エンジンが実行時に作成される Bean をインスペクションできるようにします。
Bean ファクトリ初期化 AOT コントリビューション
このステップに参加するコンポーネントは、BeanFactoryInitializationAotProcessor
(Javadoc) インターフェースを実装できます。各実装は、Bean ファクトリの状態に基づいて、AOT コントリビューションを返すことができます。
AOT コントリビューションは、特定の動作を再現する生成コードに貢献するコンポーネントです。また、リフレクション、リソースのロード、直列化、JDK プロキシの必要性を示すために RuntimeHints
に貢献することもできます。
BeanFactoryInitializationAotProcessor
実装は、インターフェースの完全修飾名に等しいキーを使用して META-INF/spring/aot.factories
に登録できます。
BeanFactoryInitializationAotProcessor
は、Bean によって直接実装することもできます。このモードでは、Bean は通常のランタイムで提供される機能と同等の AOT 貢献を提供します。そのような Bean は、AOT 最適化コンテキストから自動的に除外されます。
Bean が |
Bean 登録 AOT への貢献
コア BeanFactoryInitializationAotProcessor
実装は、各候補 BeanDefinition
に必要な貢献を収集する責任があります。専用の BeanRegistrationAotProcessor
を使用してこれを行います。
このインターフェースは次のように使用されます。
実行時の動作を置き換えるために、
BeanPostProcessor
Bean によって実装されます。たとえば、AutowiredAnnotationBeanPostProcessor
はこのインターフェースを実装して、@Autowired
でアノテーションが付けられたメンバーを挿入するコードを生成します。インターフェースの完全修飾名と等しいキーを持つ
META-INF/spring/aot.factories
に登録された型によって実装されます。通常、コアフレームワークの特定の機能に合わせて Bean 定義を調整する必要がある場合に使用されます。
Bean が |
BeanRegistrationAotProcessor
が特定の登録済み Bean を処理しない場合、デフォルトの実装がそれを処理します。Bean 定義用に生成されたコードのチューニングはコーナーケースに制限する必要があるため、これがデフォルトの動作です。
前の例で、DataSourceConfiguration
が次のようになっているとします。
Java
@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {
@Bean
public SimpleDataSource dataSource() {
return new SimpleDataSource();
}
}
このクラスには特に条件がないため、dataSourceConfiguration
と dataSource
が候補として識別されます。AOT エンジンは、上記の構成クラスを次のようなコードに変換します。
Java
/**
* Bean definitions for {@link DataSourceConfiguration}
*/
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 定義の正確な性質によって異なる場合があります。 |
上記の生成されたコードは、@Configuration
クラスと同等の Bean 定義を作成しますが、直接的な方法で、可能な限りリフレクションを使用しません。dataSourceConfiguration
用の Bean 定義と dataSourceBean
用の Bean 定義があります。datasource
インスタンスが必要な場合は、BeanInstanceSupplier
が呼び出されます。このサプライヤーは、dataSourceConfiguration
Bean で dataSource()
メソッドを呼び出します。
ベストプラクティス
AOT エンジンは、アプリケーションのコードを変更することなく、できるだけ多くのユースケースを処理できるように設計されています。ただし、一部の最適化は Bean の静的定義に基づいてビルド時に行われることに注意してください。
このセクションでは、アプリケーションが AOT に対応できるようにするためのベストプラクティスを示します。
最も高精度な 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
の使用を検討してください。
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 定義がプログラムで登録されている場合は、必ず次の手順に従ってください。
RootBeanDefinition
を使用してください。beanClass
をFactoryBean
クラスに設定して、AOT が中間層であることを認識できるようにします。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] (英語) ファイルを参照してください。