環境抽象

Environment 介面是一個整合在容器中的抽象,用於建模應用程式環境的兩個關鍵方面: profile properties

Profile 是一個命名的邏輯 Bean 定義組,只有當給定的 profile 處於啟用狀態時,才會註冊到容器中。無論是在 XML 中定義還是使用註解定義,Bean 都可以分配給一個 profile。Environment 物件與 profile 相關的作用在於確定哪些 profile(如果有)當前處於啟用狀態,以及哪些 profile(如果有)應該預設處於啟用狀態。

Properties 在幾乎所有應用中都扮演著重要角色,並且可以來自各種來源:屬性檔案、JVM 系統屬性、系統環境變數、JNDI、servlet 上下文引數、臨時的 Properties 物件、Map 物件等等。Environment 物件與 properties 相關的作用在於為使用者提供一個便捷的服務介面,用於配置屬性源並從中解析屬性。

Bean 定義 Profile

Bean 定義 Profile 在核心容器中提供了一種機制,允許在不同的環境中註冊不同的 Bean。“environment” 這個詞對不同的使用者可能意味著不同的東西,這個特性可以幫助處理許多用例,包括

  • 在開發環境中使用記憶體資料庫,而在 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 使用者設計了多種方法來完成此操作,通常依賴於系統環境變數和包含 ${placeholder} 標記的 XML <import/> 語句的組合,這些標記根據環境變數的值解析為正確的配置檔案路徑。Bean 定義 Profile 是一個核心容器特性,為這個問題提供瞭解決方案。

如果我們概括前面示例中特定於環境的 Bean 定義的用例,我們會發現需要在某些上下文中註冊某些 Bean 定義,而在其他上下文中則不需要。你可以說你想在情況 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 = "") 停用預設的銷燬方法推斷。
@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 型別。

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

  • ! : profile 的邏輯非

  • & : profile 的邏輯與

  • | : profile 的邏輯或

不能不使用括號混合使用 &| 運算子。例如,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`,除非指定的一個或多個 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` 的 name 屬性指向同一個 Bean 名,如前面示例所示。如果引數簽名都相同(例如,所有變體都有無引數工廠方法),那麼這是在有效的 Java 類中表示這種安排的唯一方法(因為特定名稱和引數簽名只能有一個方法)。

XML Bean 定義 Profile

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 對應方不支援前面描述的 profile 表示式。然而,可以使用 ! 運算子否定一個 profile。也可以透過巢狀 profile 來應用邏輯“與”,如下例所示

<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 這兩個 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 上下文引數甚至 JNDI 中的條目來指定(參見 PropertySource 抽象 )。在整合測試中,可以使用 spring-test 模組中的 `@ActiveProfiles` 註解來宣告活動的 Profile(參見 使用環境 Profile 的上下文配置 )。

注意,Profile 並非“非此即彼”的選擇。你可以同時啟用多個 Profile。透過程式設計方式,你可以向接受 String…​ 可變引數的 setActiveProfiles() 方法提供多個 Profile 名稱。以下示例激活了多個 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() 方法來更改預設 Profile 的名稱,或者透過 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 config、servlet context 引數以及如果 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 註解提供了一種方便的宣告機制,用於向 Spring 的 Environment 新增 PropertySource

給定一個名為 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>