本章將描述一下Spring中針對(duì)環(huán)境的抽象。
Environment是一個(gè)集成到容器之中的特殊抽象,它針對(duì)應(yīng)用的環(huán)境建立了兩個(gè)關(guān)鍵的概念:profile和properties.
profile是命名好的,其中包含了多個(gè)Bean的定義的一個(gè)邏輯集合,只有當(dāng)指定的profile被激活的時(shí)候,其中的Bean才會(huì)激活。無(wú)論是通過(guò)XML定義的還是通過(guò)注解解析的Bean都可以配置到profile之中。而Environment對(duì)象的角色就是跟profile相關(guān)聯(lián),然后決定來(lái)激活哪一個(gè)profile,還有哪一個(gè)profile為默認(rèn)的profile。
properties在幾乎所有的應(yīng)用當(dāng)中都有著重要的作用,當(dāng)然也可能導(dǎo)致多個(gè)數(shù)據(jù)源:property文件,JVM系統(tǒng)property,系統(tǒng)環(huán)境變量,JNDI,servlet上下文參數(shù),ad-hoc屬性對(duì)象,Map等。Environment對(duì)象和property相關(guān)聯(lián),然后來(lái)給開(kāi)發(fā)者一個(gè)方便的服務(wù)接口來(lái)配置這些數(shù)據(jù)源,并正確解析。
Bean定義的profile
在容器之中,Bean定義profile是一種允許不同環(huán)境注冊(cè)不同bean的機(jī)制。環(huán)境的概念就意味著不同的東西對(duì)應(yīng)不同的開(kāi)發(fā)者,而且這個(gè)特性能夠在一下的一些場(chǎng)景很有效:
- 解決一些內(nèi)存中的數(shù)據(jù)源的問(wèn)題,可以在不同環(huán)境訪問(wèn)不同的數(shù)據(jù)源,開(kāi)發(fā)環(huán)境,QA測(cè)試環(huán)境,生產(chǎn)環(huán)境等。
- 僅僅在開(kāi)發(fā)環(huán)境來(lái)使用一些監(jiān)視服務(wù)
- 在不同的環(huán)境,使用不同的bean實(shí)現(xiàn)
下面參考一個(gè)例子,下面的應(yīng)用需要一個(gè)DataSource,在一個(gè)測(cè)試的環(huán)境下,可能類似如下代碼:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
現(xiàn)在考慮如果應(yīng)用部署到QA環(huán)境或者生產(chǎn)環(huán)境,假設(shè)應(yīng)用的數(shù)據(jù)源是服務(wù)器上的JNDI目錄的話,我們的DataSource可能會(huì)如下:
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
問(wèn)題就是如何基于當(dāng)前的環(huán)境來(lái)使用不同的配置。過(guò)去,Spring的開(kāi)發(fā)者開(kāi)發(fā)了很多的方法來(lái)解決這個(gè)問(wèn)題,通常都依賴于系統(tǒng)環(huán)境變量和XML中的<import/>標(biāo)簽以及占位符${placeholder}等來(lái)根據(jù)不同的環(huán)境解析當(dāng)前的配置文件。Bean 的 profile是容器的特性,也是該問(wèn)題的解決方案。
如果我們泛化了我們一些特殊環(huán)境下引用的bean定義,我們可以將其中指定的Bean注入到特定的context之中,而不是所有的context之中。很多開(kāi)發(fā)者就希望能夠在一種環(huán)境下使用Bean定義A,另一種情況下使用Bean定義B。
@Profile注解
@Profile注解允許開(kāi)發(fā)者來(lái)表示一個(gè)組件是否適合在當(dāng)前環(huán)境來(lái)進(jìn)行注冊(cè),只有當(dāng)在多個(gè)Profile之中,當(dāng)前的Profile是激活的時(shí)候才可以進(jìn)行注冊(cè)。使用前面的例子,代碼可以進(jìn)行如下調(diào)整:
@Configuration
@Profile("dev")
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");
}
}
@Profile注解可以當(dāng)做元注解來(lái)使用。比如,下面所定義的@Production注解就可以來(lái)替代@Profile("production"):
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
@Profile注解也可以在方法級(jí)別使用,可以聲明在包含@Bean注解的方法之上:
@Configuration
public class AppConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
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
@Profile("production")
public DataSource productionDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
如果配置了
@Configuration的類同時(shí)配置了@Profile,那么所有的配置了@Bean注解的方法和@Import注解的相關(guān)的類都會(huì)被傳遞為該Profile除非這個(gè)Profile激活了,否則Bean定義都不會(huì)激活。如果配置為@Component或者@Configuration的類標(biāo)記了@Profile({"p1", "p2"}),那么這個(gè)類當(dāng)且僅當(dāng)Profile是p1或者p2的時(shí)候才會(huì)激活。如果某個(gè)Profile的前綴是!這個(gè)否操作符,那么@Profile注解的類會(huì)只有當(dāng)前的Profile沒(méi)有激活的時(shí)候才能生效。舉例來(lái)說(shuō),如果配置為@Profile({"p1", "!p2"}),那么注冊(cè)的行為會(huì)在Profile為p1或者是Profile為非p2的時(shí)候才會(huì)激活。
XML中Bean定義的profile
在XML中相對(duì)應(yīng)配置是<beans/>中的profile屬性。我們?cè)谇懊媾渲玫男畔⒖梢员恢貙懙絏ML文件之中如下:
<beans profile="dev"
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>
當(dāng)然,也可以通過(guò)嵌套<beans/>標(biāo)簽來(lái)完成定義部分:
<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="dev">
<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已經(jīng)被約束,允許使用上面例子之中的這類標(biāo)簽。這將為XML文件的配置提供更多便利。
激活profile
現(xiàn)在,我們已經(jīng)更新了配置信息來(lái)使用環(huán)境抽象,但是我們還需要告訴Spring來(lái)激活具體哪一個(gè)Profile。如果我們直接啟動(dòng)應(yīng)用的話,現(xiàn)在就回拋出NoSuchBeanDefinitionException異常,因?yàn)槿萜鲿?huì)找不到Spring的BeandataSource。
有多種方法來(lái)激活一個(gè)Profile,最直接的方式就是通過(guò)編程的方式來(lái)直接調(diào)用EnvironmentAPI,ApplicationContext中包含這個(gè)接口:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
額外的,Profile還可以通過(guò)spring.profiles.active中的屬性來(lái)通過(guò)系統(tǒng)環(huán)境變量,JVM系統(tǒng)變量,servlet上下文中的參數(shù),甚至是JNDI的一個(gè)參數(shù)等來(lái)寫入。在集成測(cè)試中,激活Profile可以通過(guò)spring-test中的@ActiveProfiles來(lái)實(shí)現(xiàn)。
需要注意的是,Profile的定義并不是一種互斥的關(guān)系,我們完全可以在同一時(shí)間激活多個(gè)Profile的。編程上來(lái)說(shuō),為setActiveProfile()方法提供多個(gè)Profile的名字即可:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
也可以通過(guò)spring.profiles.active來(lái)指定,逗號(hào)分隔的多個(gè)Profile的名字:
-Dspring.profiles.active="profile1,profile2"
默認(rèn)profile
默認(rèn)的Profile就表示默認(rèn)啟用的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();
}
}
如果沒(méi)有其他的Profile被激活,那么上面代碼定義的dataSource就會(huì)被創(chuàng)建,這種方式就是為默認(rèn)情況下提供Bean定義的一種方式。一旦任何一個(gè)Profile激活了,那么默認(rèn)的Profile就不會(huì)激活。
默認(rèn)的Profile的名字可以通過(guò)Environment中的setDefaultProfiles()方法或者是通過(guò)spring.profiles.default屬性來(lái)更改。
屬性源抽象
Spring的Environment的抽象提供了一些搜索選項(xiàng),來(lái)層次化配置的源信息。具體的內(nèi)容,參考如下代碼:
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsFoo = env.containsProperty("foo");
System.out.println("Does my environment contain the 'foo' property? " + containsFoo);
在上面的代碼片段之中,我們看到一個(gè)high-level的查找Spring的foo屬性是否定義的一種方式。為了知道Spring中是否包含這個(gè)屬性,Environment對(duì)象會(huì)針對(duì)PropertySource的集合進(jìn)行查找。PropertySource是針對(duì)一些key-value的屬性對(duì)的簡(jiǎn)單抽象,而Spring的StandardEnvironment是由兩個(gè)PropertySource對(duì)象所組成的,一個(gè)代表的是JVM的系統(tǒng)屬性(可以通過(guò)System.getProperties()來(lái)獲取),而另一種則是系統(tǒng)的環(huán)境變量(通過(guò)System.getenv()來(lái)獲取。)
這些默認(rèn)的屬性源都是
StandardEnvironment的代表,可以用在任何應(yīng)用之中。StandardServletEnvironment則是包含Servlet配置的環(huán)境信息,其中會(huì)包含很多Servlet的配置和Servlet上下文參數(shù)。StandardPortletEnvironment類似于StandardServletEnvironment,能夠配置portlet上下文參數(shù)??梢詤⒖计銳avadoc了解更多信息。
具體的說(shuō),當(dāng)使用StandardEnvironment的時(shí)候,調(diào)用env.containsProperty("foo")將返回一個(gè)foo的系統(tǒng)屬性,或者是foo的運(yùn)行時(shí)環(huán)境變量。
查詢配置屬性是按層次來(lái)查詢的。默認(rèn)情況下,系統(tǒng)屬性優(yōu)優(yōu)于系統(tǒng)環(huán)境變量,所以如果
foo屬性在兩個(gè)環(huán)境中都有配置的話,那么在調(diào)用env.getProperty("foo")期間,系統(tǒng)屬性值會(huì)優(yōu)先返回。需要注意的是,屬性的值是不會(huì)合并的,而是完全覆蓋掉。
在一個(gè)普通的StandardServletEnvironment之中,查找的順序如下,優(yōu)先查找* ServletConfig參數(shù)(比如DispatcherServlet上下文),然后是* ServletContext參數(shù)(web.xml中的上下文參數(shù)),再然后是* JNDI環(huán)境變量,JVM系統(tǒng)變量("-D"命令行參數(shù))以及JVM環(huán)境變量(操作系統(tǒng)環(huán)境變量)。
最重要的是,整個(gè)的機(jī)制是可以配置的。也許開(kāi)發(fā)者自己有些定義的配置源信息想集成到配置檢索的系統(tǒng)中去。沒(méi)問(wèn)題,只要實(shí)現(xiàn)開(kāi)發(fā)者自己的PropertySource并且將其加入到當(dāng)前Environment的PropertySources之中即可:
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
在上面的代碼之中,MyPropertySource被添加到檢索配置的第一優(yōu)先級(jí)之中。如果存在一個(gè)foo屬性,它將由于其他的PropertySource之中的foo屬性優(yōu)先返回。MutablePropertySourcesAPI提供一些方法來(lái)允許精確控制配置源。
@PropertySource注解
@PropertySource注解提供了一種方便的機(jī)制來(lái)將PropertySource增加到Spring的Environment之中。
給定一個(gè)文件app.properties包含了key-value對(duì)testbean.name=myTestBean,下面的代碼中,使用了@PropertySource調(diào)用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之中形如${...}的占位符,都可以被解析成Environment中的屬性資源,比如:
@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;
}
}
假設(shè)上面的my.placeholder是我們已經(jīng)注冊(cè)到Environment之中的資源,舉例來(lái)說(shuō),JVM系統(tǒng)屬性或者是環(huán)境變量的話,占位符會(huì)解析成對(duì)象的值。如果沒(méi)有的話,default/path會(huì)來(lái)作為默認(rèn)值。如果沒(méi)有指定默認(rèn)值,而且占位符也解析不出來(lái)的話,就會(huì)拋出IllegalArgumentException。
占位符解析
從歷史上來(lái)說(shuō),占位符的值是只能針對(duì)JVM系統(tǒng)屬性或者環(huán)境變量來(lái)解析的。但是現(xiàn)在不是了,因?yàn)榄h(huán)境抽象已經(jīng)繼承到了容器之中,現(xiàn)在很容易將占位符解析。這意味著開(kāi)發(fā)者可以任意的配置占位符:
- 調(diào)整系統(tǒng)變量還有環(huán)境變量的優(yōu)先級(jí)
- 增加自己的屬性源信息
具體的說(shuō),下面的XML配置不會(huì)在意customer屬性在哪里定義,只有這個(gè)值在Environment之中有效即可:
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>