環境の抽象化

Environment (Javadoc) インターフェースは、アプリケーション環境の 2 つの重要な側面(プロファイルプロパティ)をモデル化するコンテナーに統合された抽象化です。

プロファイルは、指定されたプロファイルがアクティブな場合にのみ、コンテナーに登録される Bean 定義の名前付きの論理グループです。Bean は、XML で定義されているかアノテーション付きで定義されているかに関係なく、プロファイルに割り当てることができます。プロファイルに関連する Environment オブジェクトのロールは、現在アクティブなプロファイル(存在する場合)、およびデフォルトでアクティブにするプロファイル(存在する場合)を決定することです。

プロパティは、ほとんどすべてのアプリケーションで重要なロールを果たし、プロパティファイル、JVM システムプロパティ、システム環境変数、JNDI、サーブレットコンテキストパラメーター、アドホック Properties オブジェクト、Map オブジェクトなど、さまざまなソースに由来する場合があります。プロパティに関連する Environment オブジェクトのロールは、プロパティソースを設定し、プロパティソースからプロパティを解決するための便利なサービスインターフェースをユーザーに提供することです。

Bean 定義プロファイル

Bean 定義プロファイルは、さまざまな環境でさまざまな Bean を登録できるコアコンテナーのメカニズムを提供します。「環境」という言葉は、ユーザーごとに異なることを意味し、この機能は次のような多くのユースケースに役立ちます。

  • 開発中のメモリ内データソースに対して作業することと、QA または本番環境で JNDI から同じデータソースを検索すること。

  • アプリケーションをパフォーマンス環境にデプロイするときにのみ、監視インフラストラクチャを登録します。

  • 顧客 A と顧客 B デプロイの Bean のカスタマイズされた実装を登録します。

DataSource を必要とする実用的なアプリケーションでの最初のユースケースを考えてください。テスト環境では、構成は次のようになります。

  • Java

  • Kotlin

@Bean
public DataSource dataSource() {
	return new EmbeddedDatabaseBuilder()
		.setType(EmbeddedDatabaseType.HSQL)
		.addScript("my-schema.sql")
		.addScript("my-test-data.sql")
		.build();
}
@Bean
fun dataSource(): DataSource {
	return EmbeddedDatabaseBuilder()
			.setType(EmbeddedDatabaseType.HSQL)
			.addScript("my-schema.sql")
			.addScript("my-test-data.sql")
			.build()
}

次に、アプリケーションのデータソースが本番アプリケーションサーバーの JNDI ディレクトリに登録されていると仮定して、このアプリケーションを QA または本番環境にデプロイする方法を検討します。dataSource Bean は、次のようになりました。

  • Java

  • Kotlin

@Bean(destroyMethod = "")
public DataSource dataSource() throws Exception {
	Context ctx = new InitialContext();
	return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
	val ctx = InitialContext()
	return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}

問題は、現在の環境に基づいて、これら 2 つのバリエーションの使用を切り替える方法です。時間の経過とともに、Spring ユーザーはこれを実現するためのいくつかの方法を考案しました。通常、システム環境変数と、環境変数の値に応じて正しい構成ファイルパスに解決される ${placeholder} トークンを含む XML <import/> ステートメントの組み合わせに依存しています。Bean 定義プロファイルは、この問題の解決策を提供するコアコンテナー機能です。

上記の環境固有の Bean 定義の例に示されているユースケースを一般化すると、特定のコンテキストでは特定の Bean 定義を登録する必要がありますが、他のコンテキストでは登録しません。状況 A で Bean 定義の特定のプロファイルを登録し、状況 B で別のプロファイルを登録すると言うことができます。このニーズを反映するように構成を更新することから始めます。

@Profile を使用する

@Profile (Javadoc) アノテーションを使用すると、1 つ以上の指定されたプロファイルがアクティブであるときに、コンポーネントが登録に適格であることを示すことができます。前述の例を使用して、dataSource 構成を次のように書き換えることができます。

  • Java

  • Kotlin

@Configuration
@Profile("development")
public class StandaloneDataConfig {

	@Bean
	public DataSource dataSource() {
		return new EmbeddedDatabaseBuilder()
			.setType(EmbeddedDatabaseType.HSQL)
			.addScript("classpath:com/bank/config/sql/schema.sql")
			.addScript("classpath:com/bank/config/sql/test-data.sql")
			.build();
	}
}
@Configuration
@Profile("development")
class StandaloneDataConfig {

	@Bean
	fun dataSource(): DataSource {
		return EmbeddedDatabaseBuilder()
				.setType(EmbeddedDatabaseType.HSQL)
				.addScript("classpath:com/bank/config/sql/schema.sql")
				.addScript("classpath:com/bank/config/sql/test-data.sql")
				.build()
	}
}
  • Java

  • Kotlin

@Configuration
@Profile("production")
public class JndiDataConfig {

	@Bean(destroyMethod = "") (1)
	public DataSource dataSource() throws Exception {
		Context ctx = new InitialContext();
		return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
	}
}
1@Bean(destroyMethod = "") は、デフォルトの destroy メソッドの推論を無効にします。
@Configuration
@Profile("production")
class JndiDataConfig {

	@Bean(destroyMethod = "") (1)
	fun dataSource(): DataSource {
		val ctx = InitialContext()
		return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
	}
}
1@Bean(destroyMethod = "") は、デフォルトの destroy メソッドの推論を無効にします。
前述のように、@Bean メソッドでは、通常、プログラムによる JNDI ルックアップを使用することを選択します。これには、Spring の JndiTemplate/JndiLocatorDelegate ヘルパーまたは前に示したストレート JNDI InitialContext の使用箇所を使用しますが、JndiObjectFactoryBean バリアントは使用しないため、戻り値の型を FactoryBean 型。

プロファイル文字列には、単純なプロファイル名(たとえば、production)またはプロファイル式を含めることができます。プロファイル式を使用すると、より複雑なプロファイルロジックを表現できます(たとえば、production & us-east)。プロファイル式では次の演算子がサポートされています。

  • !: プロファイルの論理 NOT 

  • &: プロファイルの論理 AND 

  • |: プロファイルの論理 OR 

括弧を使用しないと、& 演算子と | 演算子を混在させることはできません。例: production & us-east | eu-central は有効な式ではありません。production & (us-east | eu-central) として表現する必要があります。

カスタム合成アノテーションを作成する目的で、@Profile をメタアノテーションとして使用できます。次の例では、@Profile("production") のドロップイン置換として使用できるカスタム @Production アノテーションを定義します。

  • Java

  • Kotlin

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Profile("production")
annotation class Production
@Configuration クラスが @Profile でマークされている場合、そのクラスに関連付けられている @Bean メソッドと @Import アノテーションはすべて、指定されたプロファイルの 1 つ以上がアクティブでない限りバイパスされます。@Component または @Configuration クラスが @Profile({"p1", "p2"}) でマークされている場合、そのクラスは、プロファイル "p1" または "p2" がアクティブ化されていない限り、登録または処理されません。特定のプロファイルの前に NOT 演算子(!)が付いている場合、アノテーション付き要素は、プロファイルがアクティブでない場合にのみ登録されます。例: @Profile({"p1", "!p2"}) を指定すると、プロファイル 'p1' がアクティブな場合、またはプロファイル 'p2' がアクティブでない場合に登録が行われます。

次の例に示すように、@Profile はメソッドレベルで宣言して、構成クラスの特定の Bean を 1 つだけ含めることもできます(たとえば、特定の Bean の代替バリアント)。

  • Java

  • Kotlin

@Configuration
public class AppConfig {

	@Bean("dataSource")
	@Profile("development") (1)
	public DataSource standaloneDataSource() {
		return new EmbeddedDatabaseBuilder()
			.setType(EmbeddedDatabaseType.HSQL)
			.addScript("classpath:com/bank/config/sql/schema.sql")
			.addScript("classpath:com/bank/config/sql/test-data.sql")
			.build();
	}

	@Bean("dataSource")
	@Profile("production") (2)
	public DataSource jndiDataSource() throws Exception {
		Context ctx = new InitialContext();
		return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
	}
}
1standaloneDataSource メソッドは、development プロファイルでのみ使用可能です。
2jndiDataSource メソッドは、production プロファイルでのみ使用可能です。
@Configuration
class AppConfig {

	@Bean("dataSource")
	@Profile("development") (1)
	fun standaloneDataSource(): DataSource {
		return EmbeddedDatabaseBuilder()
				.setType(EmbeddedDatabaseType.HSQL)
				.addScript("classpath:com/bank/config/sql/schema.sql")
				.addScript("classpath:com/bank/config/sql/test-data.sql")
				.build()
	}

	@Bean("dataSource")
	@Profile("production") (2)
	fun jndiDataSource() =
		InitialContext().lookup("java:comp/env/jdbc/datasource") as DataSource
}
1standaloneDataSource メソッドは、development プロファイルでのみ使用可能です。
2jndiDataSource メソッドは、production プロファイルでのみ使用可能です。

@Bean メソッドの @Profile では、特別なシナリオが適用される場合があります: 同じ Java メソッド名のオーバーロードされた @Bean メソッドの場合(コンストラクターのオーバーロードに類似)、@Profile 条件はすべてのオーバーロードされたメソッドで一貫して宣言される必要があります。条件に矛盾がある場合、オーバーロードされたメソッドの最初の宣言の条件のみが重要です。@Profile を使用して、特定の引数シグニチャーが別のオーバーロードされたメソッドを選択することはできません。同じ Bean のすべてのファクトリメソッド間の解決は、作成時に Spring のコンストラクター解決アルゴリズムに従います。

異なるプロファイル条件で代替 Bean を定義する場合は、前の例に示すように、@Bean name 属性を使用して、同じ Bean 名を指す別個の Java メソッド名を使用します。引数のシグネチャーがすべて同じである場合(たとえば、すべてのバリアントに引数なしのファクトリメソッドがある場合)、これが最初に有効な Java クラスでそのような配置を表す唯一の方法です(1 つしか存在できないため)特定の名前と引数署名のメソッド)。

XML Bean 定義プロファイル

対応する XML は、<beans> 要素の profile 属性です。上記のサンプル構成は、次のように 2 つの XML ファイルに書き換えることができます。

<beans profile="development"
	xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xsi:schemaLocation="...">

	<jdbc:embedded-database id="dataSource">
		<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
		<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
	</jdbc:embedded-database>
</beans>
<beans profile="production"
	xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:jee="http://www.springframework.org/schema/jee"
	xsi:schemaLocation="...">

	<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>

次の例に示すように、同じファイル内で <beans/> 要素を分割およびネストすることを回避することもできます。

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xmlns:jee="http://www.springframework.org/schema/jee"
	xsi:schemaLocation="...">

	<!-- other bean definitions -->

	<beans profile="development">
		<jdbc:embedded-database id="dataSource">
			<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
			<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
		</jdbc:embedded-database>
	</beans>

	<beans profile="production">
		<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
	</beans>
</beans>

spring-bean.xsd は、そのような要素をファイルの最後の要素としてのみ許可するように制限されています。これにより、XML ファイルが乱雑になることなく、柔軟性が得られます。

対応する XML は、前述のプロファイル式をサポートしていません。ただし、! 演算子を使用してプロファイルを無効にすることは可能です。次の例に示すように、プロファイルをネストすることで論理的な "and" を適用することもできます。

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xmlns:jee="http://www.springframework.org/schema/jee"
	xsi:schemaLocation="...">

	<!-- other bean definitions -->

	<beans profile="production">
		<beans profile="us-east">
			<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
		</beans>
	</beans>
</beans>

上記の例では、production プロファイルと us-east プロファイルの両方がアクティブな場合、dataSource Bean が公開されています。

プロファイルの有効化

構成を更新したため、Spring にアクティブなプロファイルを指示する必要があります。サンプルアプリケーションをすぐに開始すると、コンテナーが dataSource という名前の Spring Bean を見つけられなかったため、NoSuchBeanDefinitionException がスローされます。

プロファイルのアクティブ化はいくつかの方法で実行できますが、最も簡単なのは、ApplicationContext を介して利用可能な Environment API に対してプログラムで実行することです。次の例は、その方法を示しています。

  • Java

  • Kotlin

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
val ctx = AnnotationConfigApplicationContext().apply {
	environment.setActiveProfiles("development")
	register(SomeConfig::class.java, StandaloneDataConfig::class.java, JndiDataConfig::class.java)
	refresh()
}

さらに、spring.profiles.active プロパティを使用してプロファイルを宣言的にアクティブにすることもできます。このプロパティは、システム環境変数、JVM システムプロパティ、web.xml のサーブレットコンテキストパラメーター、または JNDI のエントリとして指定できます ( PropertySource の抽象化を参照)。統合テストでは、spring-test モジュールの @ActiveProfiles アノテーションを使用してアクティブなプロファイルを宣言できます ( 「環境プロファイルによるコンテキスト構成」を参照)。

プロファイルは「どちらか一方」の命題ではないことに注意してください。複数のプロファイルを一度にアクティブ化できます。プログラムにより、String…​ 可変引数を受け入れる setActiveProfiles() メソッドに複数のプロファイル名を提供できます。次の例では、複数のプロファイルをアクティブにします。

  • Java

  • Kotlin

ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
ctx.getEnvironment().setActiveProfiles("profile1", "profile2")

宣言的に、spring.profiles.active は、次の例に示すように、プロファイル名のコンマ区切りリストを受け入れる場合があります。

-Dspring.profiles.active="profile1,profile2"

デフォルトプロファイル

デフォルトのプロファイルは、アクティブなプロファイルがない場合に有効になるプロファイルを表します。次の例を考えてみましょう。

  • Java

  • Kotlin

@Configuration
@Profile("default")
public class DefaultDataConfig {

	@Bean
	public DataSource dataSource() {
		return new EmbeddedDatabaseBuilder()
			.setType(EmbeddedDatabaseType.HSQL)
			.addScript("classpath:com/bank/config/sql/schema.sql")
			.build();
	}
}
@Configuration
@Profile("default")
class DefaultDataConfig {

	@Bean
	fun dataSource(): DataSource {
		return EmbeddedDatabaseBuilder()
				.setType(EmbeddedDatabaseType.HSQL)
				.addScript("classpath:com/bank/config/sql/schema.sql")
				.build()
	}
}

アクティブなプロファイルがない場合は、dataSource が作成されます。これは、1 つ以上の Bean にデフォルトの定義を提供する方法として見ることができます。いずれかのプロファイルが有効になっている場合、デフォルトのプロファイルは適用されません。

デフォルトのプロファイルの名前は default です。デフォルトのプロファイルの名前は、Environment で setDefaultProfiles() を使用するか、spring.profiles.default プロパティを宣言的に使用することによって変更できます。

PropertySource の抽象化

Spring の Environment 抽象化は、プロパティソースの構成可能な階層に対する検索操作を提供します。次のリストを検討してください。

  • Java

  • Kotlin

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);
val ctx = GenericApplicationContext()
val env = ctx.environment
val containsMyProperty = env.containsProperty("my-property")
println("Does my environment contain the 'my-property' property? $containsMyProperty")

上記のスニペットでは、my-property プロパティが現在の環境に定義されているかどうかを Spring に確認する高レベルの方法を示しています。この質問に答えるために、Environment オブジェクトは PropertySource (Javadoc) オブジェクトのセットに対して検索を実行します。PropertySource はキーと値のペアのソースに対する単純な抽象化であり、Spring の StandardEnvironment (Javadoc) は 2 つの PropertySource オブジェクトで構成されます。1 つは JVM システムプロパティのセット(System.getProperties())を表し、もう 1 つはシステム環境変数のセット(System.getenv())を表します

これらの既定のプロパティソースは、スタンドアロンアプリケーションで使用するために StandardEnvironment に存在します。StandardServletEnvironment (Javadoc) には、サーブレット構成、サーブレットコンテキストパラメーター、および JNDI が使用可能な場合は JndiPropertySource (Javadoc) を含む追加のデフォルトプロパティソースが取り込まれます。

具体的には、StandardEnvironment を使用すると、my-property システムプロパティまたは my-property 環境変数が実行時に存在する場合、env.containsProperty("my-property") の呼び出しは true を返します。

実行される検索は階層的です。デフォルトでは、システムプロパティは環境変数よりも優先されます。そのため、env.getProperty("my-property") の呼び出し中に両方の場所で my-property プロパティが設定された場合、システムプロパティ値が「勝ち」返されます。プロパティ値はマージされず、前のエントリによって完全にオーバーライドされることに注意してください。

一般的な StandardServletEnvironment の場合、完全な階層は次のようになり、最上位のエントリが最上位になります。

  1. ServletConfig パラメーター (該当する場合 — たとえば、DispatcherServlet コンテキストの場合)

  2. ServletContext パラメーター (web.xml context-param エントリ)

  3. JNDI 環境変数 (java:comp/env/ エントリ)

  4. JVM システムプロパティ (-D コマンドライン引数)

  5. JVM システム環境 (オペレーティングシステムの環境変数)

最も重要なことは、メカニズム全体を構成できることです。おそらく、この検索に統合したいプロパティのカスタムソースがあります。これを行うには、独自の PropertySource を実装およびインスタンス化し、現在の Environment の PropertySources のセットに追加します。次の例は、その方法を示しています。

  • Java

  • Kotlin

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
val ctx = GenericApplicationContext()
val sources = ctx.environment.propertySources
sources.addFirst(MyPropertySource())

上記のコードでは、MyPropertySource が検索で最高の優先度で追加されています。my-property プロパティが含まれている場合、他の PropertySource の my-property プロパティを優先して、プロパティが検出されて返されます。MutablePropertySources (Javadoc) API は、プロパティソースのセットの正確な操作を可能にする多くのメソッドを公開します。

@PropertySource を使用する

@PropertySource (Javadoc) アノテーションは、PropertySource を Spring の Environment に追加するための便利で宣言的なメカニズムを提供します。

キーと値のペア testbean.name=myTestBean を含む app.properties というファイルがある場合、次の @Configuration クラスは @PropertySource を使用して、testBean.getName() の呼び出しが myTestBean を返すようにします。

  • Java

  • Kotlin

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

 @Autowired
 Environment env;

 @Bean
 public TestBean testBean() {
  TestBean testBean = new TestBean();
  testBean.setName(env.getProperty("testbean.name"));
  return testBean;
 }
}
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
class AppConfig {

	@Autowired
	private lateinit var env: Environment

	@Bean
	fun testBean() = TestBean().apply {
		name = env.getProperty("testbean.name")!!
	}
}

次の例に示すように、@PropertySource リソースの場所に存在する ${…​} プレースホルダーは、環境に対してすでに登録されているプロパティソースのセットに対して解決されます。

  • Java

  • Kotlin

@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {

 @Autowired
 Environment env;

 @Bean
 public TestBean testBean() {
  TestBean testBean = new TestBean();
  testBean.setName(env.getProperty("testbean.name"));
  return testBean;
 }
}
@Configuration
@PropertySource("classpath:/com/\${my.placeholder:default/path}/app.properties")
class AppConfig {

	@Autowired
	private lateinit var env: Environment

	@Bean
	fun testBean() = TestBean().apply {
		name = env.getProperty("testbean.name")!!
	}
}

my.placeholder がすでに登録されているプロパティソース(システムプロパティや環境変数など)のいずれかに存在すると仮定すると、プレースホルダーは対応する値に解決されます。そうでない場合は、default/path がデフォルトとして使用されます。デフォルトが指定されておらず、プロパティを解決できない場合、IllegalArgumentException がスローされます。

@PropertySource は反復可能なアノテーションとして使用できます。@PropertySource は、属性オーバーライドを持つカスタム合成アノテーションを作成するためのメタアノテーションとしても使用できます。

ステートメントのプレースホルダー解決

従来、要素内のプレースホルダーの値は、JVM システムプロパティまたは環境変数に対してのみ解決できました。これはもはや事実ではありません。Environment 抽象化はコンテナー全体に統合されているため、プレースホルダの解決をコンテナー全体に簡単にルーティングできます。これは、任意の方法で解決プロセスを構成できることを意味します。システムプロパティと環境変数を検索する優先順位を変更したり、完全に削除したりできます。必要に応じて、独自のプロパティソースをミックスに追加することもできます。

具体的には、Environment で使用可能な限り、customer プロパティが定義されている場所に関係なく、次のステートメントが機能します。

<beans>
	<import resource="com/bank/service/${customer}-config.xml"/>
</beans>