環境抽象

Environment 介面是一個整合在容器中的抽象,它建模了應用程式環境的兩個關鍵方面:配置檔案屬性

配置檔案是命名過的、邏輯上的 bean 定義組,只有當給定的配置檔案處於活動狀態時,這些定義才會被註冊到容器中。無論是在 XML 中還是透過註解定義,bean 都可以被分配到某個配置檔案。Environment 物件與配置檔案相關的角色是確定哪些配置檔案(如果有)當前是活動的,以及哪些配置檔案(如果有)應該預設是活動的。

屬性在幾乎所有應用程式中都扮演著重要角色,並且可能來源於各種來源:屬性檔案、JVM 系統屬性、系統環境變數、JNDI、Servlet 上下文引數、即席 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()
}

現在考慮如何將此應用程式部署到 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} 令牌,這些令牌根據環境變數的值解析為正確的配置檔案路徑。Bean 定義配置檔案是核心容器功能,為此問題提供瞭解決方案。

如果我們泛化前面示例中顯示的環境特定 bean 定義的用例,我們最終需要註冊某些 bean 定義在某些上下文中,而不是在其他上下文中。可以說,您希望在情況 A 中註冊某種 bean 定義配置檔案,而在情況 B 中註冊不同的配置檔案。我們首先更新配置以反映這一需求。

使用 @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 = "") 停用預設銷燬方法推斷。
@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 = "") 停用預設銷燬方法推斷。
如前所述,使用 @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 用作元註解,以建立自定義組合註解。以下示例定義了一個自定義 @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,則除非一個或多個指定配置檔案處於活動狀態,否則與該類關聯的所有 @Bean 方法和 @Import 註解都將被繞過。如果一個 @Component@Configuration 類被標記為 @Profile({"p1", "p2"}),則除非配置檔案 'p1' 或 'p2' 已啟用,否則該類將不會被註冊或處理。如果給定的配置檔案以 NOT 運算子(!)為字首,則僅當該配置檔案不活動時才註冊帶註解的元素。例如,給定 @Profile({"p1", "!p2"}),如果配置檔案 'p1' 處於活動狀態或配置檔案 '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 配置檔案中可用。
2 jndiDataSource 方法僅在 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
}
1 standaloneDataSource 方法僅在 development 配置檔案中可用。
2 jndiDataSource 方法僅在 production 配置檔案中可用。

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

如果您想使用不同的配置檔案條件定義替代 bean,請使用不同的 Java 方法名稱,透過 @Bean 名稱屬性指向相同的 bean 名稱,如前面的示例所示。如果引數簽名都相同(例如,所有變體都具有無引數工廠方法),則這是在有效的 Java 類中表示這種安排的唯一方式(因為只有一個具有特定名稱和引數簽名的方法)。

XML Bean 定義配置檔案

XML 對應物是 <beans> 元素的 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 對應物不支援前面描述的配置檔案表示式。但是,可以使用 ! 運算子否定配置檔案。也可以透過巢狀配置檔案來應用邏輯“與”,如以下示例所示:

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

在前面的示例中,如果 productionus-east 配置檔案都處於活動狀態,則會暴露 dataSource bean。

啟用配置檔案

現在我們已經更新了配置,仍然需要指示 Spring 哪個配置檔案是活動的。如果我們現在啟動示例應用程式,我們將看到丟擲 NoSuchBeanDefinitionException,因為容器找不到名為 dataSource 的 Spring bean。

啟用配置檔案可以通過幾種方式完成,但最直接的方法是針對透過 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 中的 Servlet 上下文引數,甚至作為 JNDI 中的條目來指定(請參閱PropertySource 抽象)。在整合測試中,可以使用 spring-test 模組中的 @ActiveProfiles 註解來宣告活動配置檔案(請參閱使用環境配置檔案的上下文配置)。

請注意,配置檔案不是“非此即彼”的命題。您可以同時啟用多個配置檔案。透過程式設計方式,您可以向 setActiveProfiles() 方法提供多個配置檔名稱,該方法接受 String…​ 可變引數。以下示例啟用多個配置檔案:

  • 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。您可以將其視為為為一個或多個 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")

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

這些預設屬性源存在於 StandardEnvironment 中,用於獨立應用程式。StandardServletEnvironment 填充了額外的預設屬性源,包括 servlet 配置、servlet 上下文引數,以及如果 JNDI 可用,還會有一個JndiPropertySource

具體來說,當您使用 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,並將其新增到當前 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>
© . This site is unofficial and not affiliated with VMware.