独自の自動構成の作成

共有ライブラリを開発している会社で働いている場合、またはオープンソースまたは有償ライブラリに取り組んでいる場合は、独自の自動構成を開発することをお勧めします。自動構成クラスは、外部 jar にバンドルすることができ、Spring Boot によって引き続き取得されます。

自動構成は、自動構成コードと、使用する一般的なライブラリを提供する「スターター」に関連付けることができます。最初に、独自の自動構成を構築するために知っておく必要があることを説明し、次にカスタムスターターの作成に必要な一般的な手順に進みます

自動構成された Bean について

自動構成を実装するクラスには、@AutoConfiguration のアノテーションが付けられます。このアノテーション自体は @Configuration でメタアノテーションが付けられ、自動構成を標準の @Configuration クラスにします。追加の @Conditional アノテーションは、自動構成がいつ適用されるかを制約するために使用されます。通常、自動構成クラスは @ConditionalOnClass および @ConditionalOnMissingBean アノテーションを使用します。これにより、関連するクラスが見つかった場合、独自の @Configuration を宣言していない場合にのみ、自動構成が適用されます。

spring-boot-autoconfigure [GitHub] (英語) のソースコードを参照して、Spring が提供する @AutoConfiguration クラスを確認できます(META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports [GitHub] (英語) ファイルを参照)。

自動構成候補の特定

Spring Boot は、公開された jar 内に META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ファイルが存在するかどうかをチェックします。ファイルには、次の例に示すように、構成クラスが 1 行に 1 つのクラス名でリストされている必要があります。

com.mycorp.libx.autoconfigure.LibXAutoConfiguration
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
# 文字を使用して、インポートファイルにコメントを追加できます。
自動構成は、インポートファイルで指定することによってのみロードする必要があります。それらが特定のパッケージスペースで定義されていること、およびコンポーネントスキャンの対象にならないことを確認してください。さらに、自動構成クラスは、コンポーネントスキャンが追加のコンポーネントを検出できるようにすべきではありません。代わりに、特定の @Import アノテーションを使用する必要があります。

構成を特定の順序で適用する必要がある場合は、@AutoConfiguration (Javadoc) アノテーションまたは専用の @AutoConfigureBefore (Javadoc) および @AutoConfigureAfter (Javadoc) アノテーションで beforebeforeNameafterafterName 属性を使用できます。例: Web 固有の構成を提供する場合、クラスを WebMvcAutoConfiguration の後に適用する必要がある場合があります。

相互の直接的な知識がないはずの特定の自動構成を並べ替える場合は、@AutoConfigureOrder も使用できます。そのアノテーションは、通常の @Order アノテーションと同じセマンティックを持ちますが、自動構成クラスに専用の順序を提供します。

標準の @Configuration クラスと同様に、自動構成クラスが適用される順序は、それらの Bean が定義される順序にのみ影響します。これらの Bean が後で作成される順序は影響を受けず、各 Bean の依存関連と @DependsOn 関連によって決定されます。

条件アノテーション

ほとんどの場合、1 つ以上の @Conditional アノテーションを自動構成クラスに含める必要があります。@ConditionalOnMissingBean アノテーションは、デフォルトに満足できない場合に開発者が自動構成をオーバーライドできるようにするために使用される一般的な例の 1 つです。

Spring Boot には、@Configuration クラスまたは個々の @Bean メソッドにアノテーションを付けることにより、独自のコードで再利用できる多数の @Conditional アノテーションが含まれています。これらのアノテーションは次のとおりです。

クラス条件

@ConditionalOnClass および @ConditionalOnMissingClass アノテーションにより、特定のクラスの有無に基づいて @Configuration クラスを含めることができます。アノテーションメタデータは ASM (英語) を使用して解析されるという事実により、value 属性を使用して、実際のクラスを参照できます。ただし、そのクラスは実行中のアプリケーションクラスパスに実際には表示されない場合があります。String 値を使用してクラス名を指定する場合は、name 属性も使用できます。

このメカニズムは、通常戻り型が条件のターゲットである @Bean メソッドに同じ方法を適用しません。メソッドの条件が適用される前に、JVM はクラスをロードし、クラスが処理されると失敗する可能性のあるメソッド参照を処理します。現在ではない。

このシナリオを処理するには、次の例に示すように、別個の @Configuration クラスを使用して条件を分離できます。

  • Java

  • Kotlin

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@AutoConfiguration
// Some conditions ...
public class MyAutoConfiguration {

	// Auto-configured beans ...

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(SomeService.class)
	public static class SomeServiceConfiguration {

		@Bean
		@ConditionalOnMissingBean
		public SomeService someService() {
			return new SomeService();
		}

	}

}
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration(proxyBeanMethods = false)
// Some conditions ...
class MyAutoConfiguration {

	// Auto-configured beans ...
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(SomeService::class)
	class SomeServiceConfiguration {

		@Bean
		@ConditionalOnMissingBean
		fun someService(): SomeService {
			return SomeService()
		}

	}

}
メタアノテーションの一部として @ConditionalOnClass または @ConditionalOnMissingClass を使用して独自の合成アノテーションを作成する場合、そのような場合はクラスを参照しないため、name を使用する必要があります。

Bean 条件

@ConditionalOnBean および @ConditionalOnMissingBean アノテーションにより、特定の Bean の有無に基づいて Bean を含めることができます。value 属性を使用して、型ごとに Bean を指定したり、name を使用して名前ごとに Bean を指定したりできます。search 属性を使用すると、Bean を検索するときに考慮する必要がある ApplicationContext 階層を制限できます。

@Bean メソッドに配置すると、次の例に示すように、ターゲット型はデフォルトでメソッドの戻り値型になります。

  • Java

  • Kotlin

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
public class MyAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean
	public SomeService someService() {
		return new SomeService();
	}

}
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration(proxyBeanMethods = false)
class MyAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean
	fun someService(): SomeService {
		return SomeService()
	}

}

上記の例では、SomeService 型の Bean が ApplicationContext に含まれていない場合、someService Bean が作成されます。

これらの条件はこれまでに処理されたものに基づいて評価されるため、Bean 定義が追加される順序には十分注意する必要があります。このため、自動構成クラスでは @ConditionalOnBean および @ConditionalOnMissingBean アノテーションのみを使用することをお勧めします(これらはユーザー定義の Bean 定義が追加された後にロードされることが保証されているため)。
@ConditionalOnBean および @ConditionalOnMissingBean は、@Configuration クラスの作成を妨げません。クラスレベルでこれらの条件を使用することと、含まれる各 @Bean メソッドをアノテーションでマークすることの唯一の違いは、条件が一致しない場合、前者は @Configuration クラスを Bean として登録できないことです。
@Bean メソッドを宣言するときは、メソッドの戻り型でできるだけ多くの型情報を提供してください。例: Bean の具象クラスがインターフェースを実装している場合、Bean メソッドの戻り型は、インターフェースではなく具象クラスである必要があります。@Bean メソッドで可能な限り多くの型情報を提供することは、Bean 条件を使用する場合に特に重要です。これは、Bean 条件の評価が、メソッドシグネチャーで使用可能な型情報にのみ依存するためです。

プロパティ条件

@ConditionalOnProperty アノテーションにより、Spring 環境プロパティに基づいて構成を含めることができます。prefix および name 属性を使用して、チェックするプロパティを指定します。デフォルトでは、存在し、false と等しくないプロパティはすべて一致します。havingValue および matchIfMissing 属性を使用して、より高度なチェックを作成することもできます。

name 属性に複数の名前が指定されている場合、条件が一致するにはすべてのプロパティがテストに合格する必要があります。

リソース条件

@ConditionalOnResource アノテーションを使用すると、特定のリソースが存在する場合にのみ構成を含めることができます。次の例に示すように、リソースは通常の Spring 規則を使用して指定できます: file:/home/user/test.dat

Web アプリケーション条件

@ConditionalOnWebApplication および @ConditionalOnNotWebApplication アノテーションを使用すると、アプリケーションが Web アプリケーションかどうかに応じて構成を含めることができます。サーブレットベースの Web アプリケーションは、Spring WebApplicationContext を使用するアプリケーション、session スコープを定義するアプリケーション、または ConfigurableWebEnvironment を持つアプリケーションです。リアクティブ Web アプリケーションは、ReactiveWebApplicationContext を使用する、または ConfigurableReactiveWebEnvironment を持つアプリケーションです。

@ConditionalOnWarDeployment および @ConditionalOnNotWarDeployment アノテーションを使用すると、アプリケーションがサーブレットコンテナーにデプロイされる従来の WAR アプリケーションであるかどうかに応じて、構成を含めることができます。この条件は、組み込み Web サーバーで実行されるアプリケーションには一致しません。

SpEL 式の条件

@ConditionalOnExpression アノテーションにより、SpEL 式の結果に基づいて構成を含めることができます。

式で Bean を参照すると、コンテキストリフレッシュ処理の非常に早い段階で Bean が初期化されます。その結果、Bean は後処理(構成プロパティのバインドなど)の対象にならず、その状態が不完全になる可能性があります。

自動構成のテスト

自動構成は、ユーザー構成(@Bean 定義および Environment カスタマイズ)、条件評価(特定のライブラリの存在)など、多くの要因の影響を受ける可能性があります。具体的には、各テストで、これらのカスタマイズの組み合わせを表す、明確に定義された ApplicationContext を作成する必要があります。ApplicationContextRunner はそれを達成するための素晴らしい方法を提供します。

ApplicationContextRunner は、ネイティブイメージでテストを実行する場合には機能しません。

ApplicationContextRunner は通常、基本の共通構成を収集するためのテストクラスのフィールドとして定義されます。次の例では、MyServiceAutoConfiguration が常に呼び出されるようにします。

  • Java

  • Kotlin

	private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
		.withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class));
	val contextRunner = ApplicationContextRunner()
		.withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration::class.java))
複数の自動構成を定義する必要がある場合、宣言はアプリケーションの実行時とまったく同じ順序で呼び出されるため、宣言を順序付ける必要はありません。

各テストでは、ランナーを使用して特定のユースケースを表すことができます。たとえば、次のサンプルはユーザー構成(UserConfiguration)を呼び出し、自動構成が適切にバックオフすることを確認します。run を呼び出すと、AssertJ で使用できるコールバックコンテキストが提供されます。

  • Java

  • Kotlin

	@Test
	void defaultServiceBacksOff() {
		this.contextRunner.withUserConfiguration(UserConfiguration.class).run((context) -> {
			assertThat(context).hasSingleBean(MyService.class);
			assertThat(context).getBean("myCustomService").isSameAs(context.getBean(MyService.class));
		});
	}

	@Configuration(proxyBeanMethods = false)
	static class UserConfiguration {

		@Bean
		MyService myCustomService() {
			return new MyService("mine");
		}

	}
	@Test
	fun defaultServiceBacksOff() {
		contextRunner.withUserConfiguration(UserConfiguration::class.java)
			.run { context: AssertableApplicationContext ->
				assertThat(context).hasSingleBean(MyService::class.java)
				assertThat(context).getBean("myCustomService")
					.isSameAs(context.getBean(MyService::class.java))
			}
	}

	@Configuration(proxyBeanMethods = false)
	internal class UserConfiguration {

		@Bean
		fun myCustomService(): MyService {
			return MyService("mine")
		}

	}

次の例に示すように、Environment を簡単にカスタマイズすることもできます。

  • Java

  • Kotlin

	@Test
	void serviceNameCanBeConfigured() {
		this.contextRunner.withPropertyValues("user.name=test123").run((context) -> {
			assertThat(context).hasSingleBean(MyService.class);
			assertThat(context.getBean(MyService.class).getName()).isEqualTo("test123");
		});
	}
	@Test
	fun serviceNameCanBeConfigured() {
		contextRunner.withPropertyValues("user.name=test123").run { context: AssertableApplicationContext ->
			assertThat(context).hasSingleBean(MyService::class.java)
			assertThat(context.getBean(MyService::class.java).name).isEqualTo("test123")
		}
	}

ランナーを使用して ConditionEvaluationReport を表示することもできます。レポートは、INFO または DEBUG レベルで出力できます。次の例は、ConditionEvaluationReportLoggingListener を使用して自動構成テストでレポートを出力する方法を示しています。

  • Java

  • Kotlin

import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

class MyConditionEvaluationReportingTests {

	@Test
	void autoConfigTest() {
		new ApplicationContextRunner()
			.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
			.run((context) -> {
				// Test something...
			});
	}

}
import org.junit.jupiter.api.Test
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
import org.springframework.boot.logging.LogLevel
import org.springframework.boot.test.context.assertj.AssertableApplicationContext
import org.springframework.boot.test.context.runner.ApplicationContextRunner

class MyConditionEvaluationReportingTests {

	@Test
	fun autoConfigTest() {
		ApplicationContextRunner()
			.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
			.run { context: AssertableApplicationContext? -> }
	}

}

Web コンテキストのシミュレーション

サーブレットまたはリアクティブ Web アプリケーションのコンテキストでのみ動作する自動構成をテストする必要がある場合は、それぞれ WebApplicationContextRunner または ReactiveWebApplicationContextRunner を使用してください。

クラスパスのオーバーライド

実行時に特定のクラスやパッケージが存在しない場合に何が起こるかをテストすることもできます。Spring Boot には、ランナーが簡単に使用できる FilteredClassLoader が付属しています。次の例では、MyService が存在しない場合、自動構成が適切に無効になっていると断言します。

  • Java

  • Kotlin

	@Test
	void serviceIsIgnoredIfLibraryIsNotPresent() {
		this.contextRunner.withClassLoader(new FilteredClassLoader(MyService.class))
			.run((context) -> assertThat(context).doesNotHaveBean("myService"));
	}
	@Test
	fun serviceIsIgnoredIfLibraryIsNotPresent() {
		contextRunner.withClassLoader(FilteredClassLoader(MyService::class.java))
			.run { context: AssertableApplicationContext? ->
				assertThat(context).doesNotHaveBean("myService")
			}
	}

独自のスターターを作成する

典型的な Spring Boot スターターには、特定のテクノロジーのインフラストラクチャを自動構成してカスタマイズするコードが含まれています。これを "acme" と呼びましょう。簡単に拡張できるようにするために、専用の名前空間にある多数の構成キーを環境に公開できます。最後に、ユーザーができるだけ簡単に開始できるように、単一の「スターター」依存関係が提供されています。

具体的には、カスタムスターターには次のものを含めることができます。

  • "acme" の自動構成コードを含む autoconfigure モジュール。

  • autoconfigure モジュールへの依存関係を提供する starter モジュール、および "acme" と通常役立つその他の依存関係。簡単に言えば、スターターを追加すると、そのライブラリの使用を開始するために必要なすべてが提供されます。

この 2 つのモジュールへの分離は、決して必要ではありません。"acme" にいくつかのフレーバー、オプション、オプション機能がある場合は、一部の機能がオプションであるという事実を明確に表現できるため、自動構成を分離することをお勧めします。さらに、これらのオプションの依存関係について意見を提供するスターターを作成することができます。同時に、他の人は autoconfigure モジュールのみに依存し、異なる意見で独自のスターターを作成することができます。

自動構成が比較的簡単で、オプション機能がない場合、スターターで 2 つのモジュールをマージすることは間違いなくオプションです。

ネーミング

スターターに適切な名前空間を提供するようにしてください。別の Maven groupId を使用している場合でも、モジュール名を spring-boot で始めないでください。将来的に自動構成するものについては、公式サポートを提供する場合があります。

経験則として、組み合わせたモジュールにはスターターにちなんで名前を付ける必要があります。例: "acme" のスターターを作成し、自動構成モジュールに acme-spring-boot およびスターター acme-spring-boot-starter の名前を付けると仮定します。2 つを結合するモジュールが 1 つしかない場合は、acme-spring-boot-starter という名前を付けます。

構成キー

スターターが構成キーを提供する場合は、それらに一意の名前空間を使用します。特に、Spring Boot が使用する名前空間(servermanagementspring など)にキーを含めないでください。同じ名前空間を使用している場合、将来、モジュールを壊すような方法でこれらの名前空間を変更する可能性があります。経験則として、すべてのキーの前に、所有している名前空間(acme など)を付けます。

次の例に示すように、プロパティごとにフィールド javadoc を追加して、構成キーがドキュメント化されていることを確認します。

  • Java

  • Kotlin

import java.time.Duration;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("acme")
public class AcmeProperties {

	/**
	 * Whether to check the location of acme resources.
	 */
	private boolean checkLocation = true;

	/**
	 * Timeout for establishing a connection to the acme server.
	 */
	private Duration loginTimeout = Duration.ofSeconds(3);

	// getters/setters ...

	public boolean isCheckLocation() {
		return this.checkLocation;
	}

	public void setCheckLocation(boolean checkLocation) {
		this.checkLocation = checkLocation;
	}

	public Duration getLoginTimeout() {
		return this.loginTimeout;
	}

	public void setLoginTimeout(Duration loginTimeout) {
		this.loginTimeout = loginTimeout;
	}

}
import org.springframework.boot.context.properties.ConfigurationProperties
import java.time.Duration

@ConfigurationProperties("acme")
class AcmeProperties(

	/**
	 * Whether to check the location of acme resources.
	 */
	var isCheckLocation: Boolean = true,

	/**
	 * Timeout for establishing a connection to the acme server.
	 */
	var loginTimeout:Duration = Duration.ofSeconds(3))
それらは JSON に追加される前に処理されないため、@ConfigurationProperties フィールドの Javadoc でプレーンテキストのみを使用する必要があります。

レコードクラスで @ConfigurationProperties を使用する場合、レコードコンポーネントの説明はクラスレベルの Javadoc タグ @param を介して提供される必要があります (レコードクラスには、通常のフィールドレベルの Javadoc を配置するための明示的なインスタンスフィールドはありません)。

説明が一貫していることを確認するために、内部で従ういくつかのルールを次に示します。

  • "The" または "A" で説明を始めないでください。

  • boolean 型の場合、説明を "Whether" または "Enable" で開始します。

  • コレクションベースの型の場合、説明を「カンマ区切りリスト」で開始します

  • long ではなく java.time.Duration を使用し、「期間サフィックスが指定されていない場合は秒が使用されます」など、ミリ秒と異なる場合はデフォルトの単位を記述します。

  • 実行時に決定する必要がない限り、説明にデフォルト値を指定しないでください。

キーに対して IDE 支援も利用できるように、必ずメタデータ生成をトリガーしてください。生成されたメタデータ (META-INF/spring-configuration-metadata.json) を確認して、キーが適切にドキュメント化されていることを確認することをお勧めします。互換性のある IDE で独自のスターターを使用して、メタデータの品質を検証することも良い考えです。

「自動構成」モジュール

autoconfigure モジュールには、ライブラリの使用を開始するために必要なすべてのものが含まれています。また、構成キーの定義(@ConfigurationProperties など)と、コンポーネントの初期化方法をさらにカスタマイズするために使用できるコールバックインターフェースも含まれている場合があります。

ライブラリへの依存関係をオプションとしてマークして、autoconfigure モジュールをプロジェクトに簡単に含めることができるようにする必要があります。そのようにすると、ライブラリは提供されず、デフォルトで Spring Boot はオフに戻ります。

Spring Boot は、アノテーションプロセッサーを使用して、自動構成の条件をメタデータファイル (META-INF/spring-autoconfigure-metadata.properties) に収集します。そのファイルが存在する場合は、一致しない自動構成を先行してフィルタリングするために使用され、起動時間が改善されます。

Maven でビルドする場合、自動構成を含むモジュールに次の依存関係を追加することをお勧めします。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-autoconfigure-processor</artifactId>
	<optional>true</optional>
</dependency>

アプリケーションで自動構成を直接定義した場合は、repackage ゴールが依存関係を uber jar に追加しないように、必ず spring-boot-maven-plugin を構成してください。

<project>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.springframework.boot</groupId>
							<artifactId>spring-boot-autoconfigure-processor</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

次の例に示すように、Gradle では、依存関係を annotationProcessor 構成で宣言する必要があります。

dependencies {
	annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor"
}

スターターモジュール

スターターは本当に空の jar です。その唯一の目的は、ライブラリを操作するために必要な依存関係を提供することです。それは、始めるために何が必要であるかについての意見に基づく見解と考えることができます。

スターターが追加されるプロジェクトについて想定しないでください。通常、自動構成するライブラリに他のスターターが必要な場合は、それらにもメンションしてください。オプションの依存関係の数が多い場合、ライブラリの通常の使用には不要な依存関係を含めることを避ける必要があるため、適切なデフォルトの依存関係のセットを提供するのは難しい場合があります。つまり、オプションの依存関係を含めないでください。

いずれの場合も、スターターはコア Spring Boot スターター(spring-boot-starter)を直接または間接的に参照する必要があります(スターターが別のスターターに依存している場合は、追加する必要はありません)。カスタムスターターのみを使用してプロジェクトを作成する場合、Spring Boot のコア機能は、コアスターターの存在によって尊重されます。