1.13 Environment抽象
Environment
接口是在容器中抽象集成,應用環境包括兩個重要的方面:profiles 和 properties。配置文件是一個命名的bean定義邏輯組,只有在給定的配置文件處于激活狀態時才在容器中注冊。可以將bean分配給配置文件,不管它是用XML定義的還是用注解定義的。與profile
文件相關的環境對象的角色是確定哪些profile
文件(如果有的話)當前是激活的,以及哪些profile
文件(如果有的話)在默認情況下應該是激活的。
Properties
幾乎在所有應用中扮演一個重要的角色并且可能來自于多個源:屬性文件、JVM系統屬性、系統環境變量、JNDI、servlet上下文參數、Properties對象、Map對象等等。Environment
對象的角色與properties
關聯去提供給用戶一個便利的服務接口去配置屬性源和解析屬性。
1.13.1 Bean定義Profiles
bean定義屬性文件在核心容器中提供一個機制,它允許在不同的環境中注冊不同bean。環境這個詞對于不同的用戶可能意味著不同的東西,這個特性可以幫助許多使用場景,包括:
- 在開發中針對內存中的數據源進行工作,而不是在進行QA或生產時從JNDI查找相同的數據源。
- 當部署應用到執行環境使用注冊健康基礎設施
- 為客戶A和客戶B部署注冊定制的bean實現。
在實踐應用中考慮第一個使用場景,它需要獲取一個DataSource
。在測試環境中,配置假設如下:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
現在考慮這個應用怎樣部署到QA或生產環境,假設應用程序數據源注冊在生成應用服務JNDI目錄。我們的dataSource
bean看起來類似下面清單:
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
問題是如何根據當前環境在使用這兩種變體之間進行切換。隨著時間的流逝,Spring用戶已經設計出多種方法來完成此任務,通常依賴系統環境變量和包含$ {placeholder}
占位符的XML <import/>語句的組合,這些語句根據值解析為正確的配置文件路徑環境變量。Bean定義配置文件是一項核心容器功能,可解決此問題。
如果我們概括前面環境特定的bean定義示例中所示的用例,我們最終需要在特定上下文中注冊特定的bean定義,而不是在其他上下文中注冊。可以這樣說,你希望在情形A中注冊bean定義的某個配置文件,而在情形B中注冊另一個配置文件。我們開始更新配置以反映這種需求。
使用@Profile
@Profile
注解允許你去指明哪些組件適合去注冊,當一個或多個指定profile
處于激活狀態時。使用我們前面的例子,我們可以重寫dataSource
配置如下:
@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("production")
public class JndiDataConfig {
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
如前所述,對于
@Bean
方法,你典型的選擇使用編程式的JNDI查找,通過使用Spring的JndiTemplate/JndiLocatorDelegate
幫助類或直接使用前面展示的JNDIInitialContext
,而不是使用JndiObjectFactoryBean
變體,這將迫使你將返回類型聲明為FactoryBean
類型。
配置文件字符串可能包含一個簡單的配置名稱(例如,production
)或一個配置表達式。一個配置表達式允許更復雜的配置邏輯去表達(例如,production & us-east
),下面的操作符在profile
表達式中是被支持的:
- !:邏輯非
- &:邏輯與
- |:邏輯或
你不能混合
&
和!
操作符而不使用括號。例如,production & us-east | eu-central
是無效表達式。它必須被表達類似production & (us-east | eu-central)
。
你可以使用@Profile
作為一個元數據注解去創建你自定義的注解。以下示例定義了一個自定義@Production
注解,你可以將其用作@Profile(“ production”)
的替代。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
如果
@Configuration
類被標注@Profile
,所有的@Bean
方法和@Import
注解關聯的類都將被繞過,除非一個或多個指定的配置文件被激活。如果@Component
或@Configuration
類被標記@Profile({"p1", "p2"})
,這個類不會被注冊或處理除非配置文件p1
或p2
被激活。如果給的配置前綴是NOT
操作符(!
),注解元素僅僅在配置文件沒有被激活時被注冊。例如,@Profile({"p1", "!p2"})
,如果配置p1
被激活或者配置p2
沒有被激活時注冊才會發生。
@Profile
也可以被聲明在方法級別去包含一個特定的配置bean類(例如,用于特定bean的替代),類似下面例子展示:
@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");
}
}
-
standaloneDataSource
方法僅僅在development
配置中有效 -
jndiDataSource
方法僅僅在production
配置中有效
對于
@Bean
方法上的@Profile
,可以應用一個特殊的場景:在重載相同Java方法名的@Bean
方法(類似于構造函數重載)的情況下,需要一致地在所有重載方法上聲明@Profile
條件。如果條件不一致,則只有重載方法中第一個聲明的條件重要。因此,@Profile
不能被使用去選擇具有特定參數簽名重載方法。在創建時,同一bean的所有工廠方法之間的解析都遵循Spring的構造函數解析方法。如果你想去定義不同配置條件的bean,使用不同的Java方法名稱,通過使用
@Bean
name屬性指向相同名稱的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對應項不支持前面描述的配置文件表達式:然而,它可能通過
!
操作符否定一個配置文件。它也可能通過嵌入配置文件應用邏輯and
,類型下面例子顯示:<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>
在前面的例子中,如果
production
和us-east
配置都被激活,則dataSource
bean被暴露。
激活profile
現在我們已經更新了配置文件,我們仍然需要指示Spring哪一個配置文件激活。如果我們已經啟動了我們的應用程序,我們將看到一個NoSuchBeanDefinitionException
拋出,因為容器不能找到名稱為dataSource
的bean。
可以通過多種方式來激活配置文件,但最直接的方法是可通過ApplicationContext
獲得的Environment
API以編程方式進行配置。下面例子展示怎樣去做:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
此外,你也可以聲明式地激活配置文件通過spring.profiles.active
屬性,也可通過系統環境變量、JVM屬性、在web.xml中servlet上下文參數,甚至作為JNDI中的條目(查看PropertySource
抽象)。在集成測試中,激活配置文件可以通過使用@ActiveProfiles
聲明在spring-test
模塊(查看上下文配置通過環境配置文件)。
請注意,配置文件不是非此即彼
的命題。你可以一次性激活多個配置文件。編程式地,你可以提供多個配置文件名稱給setActiveProfiles()
方法,它可以接受String…
可變參數。下面例子激活多個配置文件:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
聲明式地,spring.profiles.active
可以接收配置名稱逗號分隔列表,類似下面例子展示:
-Dspring.profiles.active="profile1,profile2"
默認profile
默認配置文件表示默認情況下啟用的配置文件。考慮下面例子:
@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();
}
}
如果沒有profile
被激活,dataSource
實例被創建。你可以看到這種方式提供一個默認的定義為一個或多個bean。如果任何profile
被激活,這個默認profile不被使用。
你可以通過在Environment
上使用setDefaultProfiles()
改變默認profile
名稱或者,聲明式地,通過使用spring.profiles.default
屬性。
1.13.2 PropertySource
抽象
Spring的Environment
抽象提供了可配置屬性源層次結構上的搜索操作。考慮下面清單:
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);
在前面的片段中,我們看到一個高級別的詢問Spring的方式,是否my-property
屬性在當前環境中被定義:去回答這個問題,Environment
對象執行搜索PropertySource
集合對象。PropertySource
是一個簡單key-value
對源的抽象,并且Spring的StandardEnvironment
被配置兩個PropertySource
對象,一個描述JVM系統(System.getProperties()
)屬性集合另一個描述系統環境變量集合(System.getenv()
)。
這些默認的屬性源適用
StandardEnvironment
,為在獨立的應用中使用。StandardServletEnvironment
是通過附加默認屬性源填充,包括servlet配置和servlet上下文參數。它可以選擇啟用JndiPropertySource
。查看javadock詳情。
具體地說,當你使用StandardEnvironment
時,如果my-property
系統屬性或my-property
環境變量在運行時被描述,調用env.containsProperty("my-property")
方法將返回true。
執行的搜索是分層的,默認情況,系統屬性優先級高于環境變量。因此,如果
my-property
屬性在兩個地方被設置,在調用env.getProperty("my-property")
時,系統屬性值將被返回。請注意,屬性值不會合并,而是會被前面的值完全覆蓋。對于通用的
StandardServletEnvironment
,完整的層次結構如下,優先級最高的條目位于頂部:
1.ServletConfig
參數(如果使用的-例如,如果是DispatcherServlet
上下文)
2.ServletContext
參數(web.xml 上下文參數)3.JNDI 環境變量(
java:comp/env/
)4.JVM系統參數(-D 命令行參數)
5.JVM系統變量(操作系統環境變量)
最重要地,整個機制是可配置的。也許你有一個自定義的屬性源,你想整合到此搜索中。這樣做,實現和實例化你的PropertySource
并且添加到當前的Environment
PropertySources
集合中。下面例子展示怎樣去做:
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
在前面的代碼中,MyPropertySource
被添加到最高索引優先級中。如果它包含my-property
屬性,則會檢測到并返回該屬性,從而支持任何其他PropertySource
中的my-property
屬性。MutablePropertySources
API暴露了一些方法,這些方法允許去精確操作屬性源集合。
1.13.3 使用@PropertySource
@PropertySource
注解提供一個便捷的和陳述式的機制去添加PropertySource
到Spring的Environment
中。
給定一個名叫app.properties
文件,它包含健值對testbean.name=myTestBean
,下面的@Configuration
類使用@PropertySource
,以這種方式調用testBean.getName()
并返回myTestBean
:
@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;
}
}
@PropertySource資源位置中出現的任何${}占位符都將根據已經在環境中注冊的屬性源集進行解析,如下面的示例所示:
@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;
}
}
假設my.placeholder
在一個已經被注冊的屬性源中被描述(例如,系統屬性或環境變量),則占位符被解析為對應值。如果沒有,則使用一個默認值default/path
。如果沒有指定默認值并且屬性不能被解析,一個IllegalArgumentException
被拋出。
根據Java8約定,
@PropertySource
注解是可以重復的。然而,所有的@PropertySource
注解需要在相同等級被聲明,要么直接地在配置類上,要么作為元數據注解在相同自定義注解中。不建議將直接注釋和元注釋混合使用,因為直接注釋會有效地覆蓋元注釋。
參考代碼:
com.liyong.ioccontainer.starter.EnvironmentIocContainer
1.13.4 語句中的占位符解析
在以前,元素中占位符的值只能根據JVM系統屬性或環境變量來解析。現在情況已經不同了。因為Environment
抽象已經集成到容器,很容易通過它來路由占位符的解析。這意味著你可以按照自己喜歡的任何方式配置解析過程。你可以更改搜索系統屬性和環境變量的優先級,也可以完全刪除它們。你還可以適當地將你自己的屬性源添加到組合中。
具體地說,不論在何處定義customer屬性,只要在環境中可用,以下語句就可以工作:
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>
作者
個人從事金融行業,就職過易極付、思建科技、某網約車平臺等重慶一流技術團隊,目前就職于某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大數據、數據存儲、自動化集成和部署、分布式微服務、響應式編程、人工智能等領域。同時也熱衷于技術分享創立公眾號和博客站點對知識體系進行分享。
CSDN:https://blog.csdn.net/liyong1028826685
微信公眾號:
技術交流群: