環境抽象
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
現在我們已經更新了配置,仍然需要指示 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。
執行的搜尋是分層次的。預設情況下,系統屬性優先於環境變數。因此,如果在呼叫 對於常見的
|
最重要的是,整個機制是可配置的。也許你有一個自定義的屬性源,你想將其整合到這個搜尋中。要做到這一點,實現並例項化你自己的 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
註解提供了一種方便的宣告機制,用於向 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>