環境抽象
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,請使用不同的 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 對應物不支援前面描述的配置檔案表示式。但是,可以使用
在前面的示例中,如果 |
啟用配置檔案
現在我們已經更新了配置,仍然需要指示 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。
|
執行的搜尋是分層的。預設情況下,系統屬性優先於環境變數。因此,如果在呼叫 對於常見的
|
最重要的是,整個機制是可配置的。也許您有自己的自定義屬性源,想要整合到這個搜尋中。為此,實現並例項化您自己的 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 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>