環境抽象概念
`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 對應項是 `
<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」,如下列範例所示:
在前面的範例中,如果 `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。
執行的搜尋是階層式的。預設情況下,系統屬性優先於環境變數。因此,如果在呼叫 對於常見的
|
最重要的是,整個機制是可配置的。也許您有想要整合到此搜尋中的自訂屬性來源。若要這樣做,請實作並實例化您自己的 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>