環境抽象概念

`Environment` 介面是在容器中整合的抽象概念,用於模擬應用程式環境的兩個主要方面:Profile屬性

Profile 是一個具名的邏輯 Bean 定義群組,只有在指定的 Profile 啟用時才會向容器註冊。無論 Bean 是在 XML 中定義還是使用註解定義,都可以將其分配給 Profile。`Environment` 物件在 Profile 方面的作用是確定目前哪些 Profile (如果有) 處於啟用狀態,以及預設應啟用哪些 Profile (如果有)。

屬性在幾乎所有應用程式中都扮演重要角色,並且可能來自各種來源:屬性檔案、JVM 系統屬性、系統環境變數、JNDI、Servlet Context 參數、Ad-hoc `Properties` 物件、`Map` 物件等等。`Environment` 物件在屬性方面的作用是為使用者提供方便的服務介面,用於組態屬性來源並從中解析屬性。

Bean 定義 Profile

Bean 定義 Profile 在核心容器中提供了一種機制,允許在不同環境中註冊不同的 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()
}

現在考慮如何將此應用程式部署到 QA 或生產環境中,假設應用程式的資料來源已在生產應用程式伺服器的 JNDI 目錄中註冊。我們的 `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
}

問題是如何根據目前的環境在兩種變體之間切換使用。隨著時間的推移,Spring 使用者設計了許多方法來完成此操作,通常依賴於系統環境變數和 XML `<import/>` 陳述式的組合,其中包含 `${placeholder}` Token,這些 Token 會根據環境變數的值解析為正確的組態檔案路徑。Bean 定義 Profile 是一個核心容器功能,可為此問題提供解決方案。

如果我們將前面範例中顯示的環境特定 Bean 定義的使用案例一般化,最終會需要在某些 Context 中註冊某些 Bean 定義,而在其他 Context 中則不註冊。您可以說您想在情況 A 中註冊特定的 Bean 定義 Profile,而在情況 B 中註冊不同的 Profile。我們從更新組態以反映此需求開始。

使用 `@Profile`

`@Profile` 註解可讓您指示當一個或多個指定的 Profile 處於啟用狀態時,組件符合註冊資格。使用我們前面的範例,我們可以將 `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` 類型。

Profile 字串可以包含簡單的 Profile 名稱 (例如,`production`) 或 Profile 運算式。Profile 運算式允許表達更複雜的 Profile 邏輯 (例如,`production & us-east`)。Profile 運算式支援以下運算子:

  • `!`:Profile 的邏輯 `NOT`

  • `&`:Profile 的邏輯 `AND`

  • `|`:Profile 的邏輯 `OR`

在不使用括號的情況下,您無法混合使用 `&` 和 `|` 運算子。例如,`production & us-east | eu-central` 不是有效的運算式。它必須表示為 `production & (us-east | eu-central)`。

您可以將 `@Profile` 用作 Meta-Annotation,以建立自訂的組合註解。以下範例定義了一個自訂的 `@Production` 註解,您可以將其用作 `@Profile(\"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`,則除非一個或多個指定的 Profile 處於啟用狀態,否則與該類別關聯的所有 `@Bean` 方法和 `@Import` 註解都會被略過。如果 `@Component` 或 `@Configuration` 類別標有 `@Profile({"p1", "p2"})`,則除非 Profile 'p1' 或 'p2' 已啟用,否則該類別不會註冊或處理。如果給定的 Profile 以 NOT 運算子 (`!`) 作為前綴,則只有在 Profile 未啟用時才會註冊註解的元素。例如,給定 `@Profile({"p1", "!p2"})`,如果 Profile 'p1' 啟用或 Profile 'p2' 未啟用,則會發生註冊。

`@Profile` 也可以在方法層級宣告,以僅包含組態類別的特定 Bean (例如,用於特定 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");
	}
}
1 `standaloneDataSource` 方法僅在 `development` Profile 中可用。
2 `jndiDataSource` 方法僅在 `production` Profile 中可用。
@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
}
1 `standaloneDataSource` 方法僅在 `development` Profile 中可用。
2 `jndiDataSource` 方法僅在 `production` Profile 中可用。

對於 `@Bean` 方法上的 `@Profile`,可能會套用特殊情境:在相同 Java 方法名稱的重載 `@Bean` 方法 (類似於建構子重載) 的情況下,需要在所有重載方法上一致地宣告 `@Profile` 條件。如果條件不一致,則只有重載方法中第一個宣告上的條件才重要。因此,`@Profile` 無法用於選擇具有特定引數簽名的重載方法來取代另一個方法。在建立時,相同 Bean 的所有工廠方法之間的解析都遵循 Spring 的建構子解析演算法。

如果您想定義具有不同 Profile 條件的替代 Bean,請使用不同的 Java 方法名稱,並使用 `@Bean` 名稱屬性指向相同的 Bean 名稱,如前面的範例所示。如果引數簽名都相同 (例如,所有變體都具有無引數工廠方法),則這是首先在有效的 Java 類別中表示此類安排的唯一方法 (因為只能有一個具有特定名稱和引數簽名的方法)。

XML Bean 定義 Profile

XML 對應項是 `` 元素的 `profile` 屬性。我們前面的範例組態可以重寫為兩個 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 對應項不支援先前描述的 Profile 運算式。但是,可以使用 `!` 運算子來否定 Profile。也可以透過巢狀 Profile 來套用邏輯「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` Profile 都處於啟用狀態,則會公開 `dataSource` Bean。

啟用 Profile

現在我們已經更新了組態,我們仍然需要指示 Spring 哪個 Profile 處於啟用狀態。如果我們現在啟動範例應用程式,我們會看到拋出 `NoSuchBeanDefinitionException`,因為容器找不到名為 `dataSource` 的 Spring Bean。

啟用 Profile 可以透過多種方式完成,但最直接的方式是透過 `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` 屬性以宣告方式啟用 Profile,該屬性可以透過系統環境變數、JVM 系統屬性、`web.xml` 中的 Servlet Context 參數指定,甚至可以作為 JNDI 中的項目 (請參閱 PropertySource 抽象概念)。在整合測試中,可以使用 `spring-test` 模組中的 `@ActiveProfiles` 註解來宣告啟用的 Profile (請參閱 使用環境 Profile 進行 Context 組態)。

請注意,Profile 不是「非此即彼」的命題。您可以一次啟用多個 Profile。以程式化方式,您可以將多個 Profile 名稱提供給 `setActiveProfiles()` 方法,該方法接受 `String…​` Varargs。以下範例啟用多個 Profile:

  • Java

  • Kotlin

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

以宣告方式,`spring.profiles.active` 可以接受逗號分隔的 Profile 名稱列表,如下列範例所示:

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

預設 Profile

預設 Profile 代表在沒有 Profile 啟用時啟用的 Profile。考慮以下範例:

  • 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()
	}
}

如果 沒有 Profile 啟用,則會建立 `dataSource`。您可以將其視為為一個或多個 Bean 提供預設定義的方式。如果啟用任何 Profile,則預設 Profile 不適用。

預設 Profile 的名稱為 `default`。您可以使用 `Environment` 上的 `setDefaultProfiles()` 或以宣告方式使用 `spring.profiles.default` 屬性來變更預設 Profile 的名稱。

`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")

在前述程式碼片段中,我們看到一種高階的方法,詢問 Spring 目前環境是否定義了 my-property 屬性。為了回答這個問題,Environment 物件會在 PropertySource 物件集合中執行搜尋。PropertySource 是對任何鍵值對來源的簡單抽象化,而 Spring 的 StandardEnvironment 配置了兩個 PropertySource 物件 — 一個代表 JVM 系統屬性集合 (System.getProperties()),另一個代表系統環境變數集合 (System.getenv())。

這些預設屬性來源存在於 StandardEnvironment 中,用於獨立應用程式。StandardServletEnvironment 則填充了額外的預設屬性來源,包括 Servlet 配置、Servlet Context 參數,以及在 JNDI 可用的情況下,會加入 JndiPropertySource

具體來說,當您使用 StandardEnvironment 時,呼叫 env.containsProperty("my-property") 會在執行時期如果存在 my-property 系統屬性或 my-property 環境變數時,傳回 true。

執行的搜尋是階層式的。預設情況下,系統屬性優先於環境變數。因此,如果在呼叫 env.getProperty("my-property") 期間,my-property 屬性剛好在兩個地方都設定了,則系統屬性值會「勝出」並被傳回。請注意,屬性值不會合併,而是完全被先前的條目覆寫。

對於常見的 StandardServletEnvironment,完整的階層結構如下,最高優先順序的條目在最上方

  1. ServletConfig 參數 (如果適用 — 例如,在 DispatcherServlet context 的情況下)

  2. ServletContext 參數 (web.xml context-param 條目)

  3. JNDI 環境變數 (java:comp/env/ 條目)

  4. JVM 系統屬性 (-D 命令列引數)

  5. JVM 系統環境 (作業系統環境變數)

最重要的是,整個機制是可配置的。也許您有想要整合到此搜尋中的自訂屬性來源。若要這樣做,請實作並實例化您自己的 PropertySource,並將其新增至目前 EnvironmentPropertySources 集合。以下範例展示如何執行此操作

  • 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 API 公開了許多方法,可精確操作屬性來源集合。

使用 @PropertySource

@PropertySource 註解提供了一種方便且宣告式的方法,可將 PropertySource 新增至 Spring 的 Environment

給定一個名為 app.properties 的檔案,其中包含鍵值對 testbean.name=myTestBean,以下 @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 抽象化已整合到整個容器中,因此很容易透過它來路由佔位符的解析。這表示您可以以任何您喜歡的方式配置解析過程。您可以變更搜尋系統屬性和環境變數的優先順序,或完全移除它們。您也可以根據需要將您自己的屬性來源新增到組合中。

具體來說,以下陳述式無論 customer 屬性定義在哪裡都有效,只要它在 Environment 中可用

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