上一章了解了一些最為核心的bean裝配技術,但是bean裝配所涉及的領域并不僅僅局限于上一章的内容,Spring提供了多種技巧,借助他們能實作更為進階的bean裝配功能。雖然這些技術不會經常用到,但這并不意味着他們的價值會是以而降低。
環境與Profile
在開發的時候有一個很大的挑戰就是将應用程式從一個環境遷移到另一個環境。舉一個栗子,比如配置資料庫(有關資料庫配置的詳細會在後面的章節)。在開發階段,我們可能會使用嵌入式資料庫,并預先加載測試資料,如下:
@Bean(destroyMethod = "shutdown")
public EmbeddedDatabase embeddedDatasource(){
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
這會建立一個類型為javax.sql.DataSource的bean,他使用EmbeddedDatabaseBuilder會搭建一個嵌入式的Hypersonic資料庫,他的模式(schema)定義在shcema.sql中,測試資料通過test-data.sql加載,這種方式對于開發環境來說是非常适合的,每次啟動他的時候都能讓資料庫處于一個給定的狀态。但是對于生産環境來說卻是一個糟糕的選擇,更多的會選擇JNDI從容器中擷取一個DataSource,對于這種情況我們應該使用如下的bean
@Bean
public DataSource jndidataDataSource(){
JndiObjectFactoryBean jndiObjectFactoryBean =
new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
通過JNDI擷取DataSource能夠讓容器決定該如何建立這個DataSource,JNDI更加适合的是生産環境,然而對于測試環境來說,會帶來不必要的複雜性。在測試環境中,可以選擇配置Commons DBCP連接配接池,具體的方式就不寫了大同小異。這是一個很好的栗子,他表現了在不同的環境中某個bean會有所不同,我們必須要有一種方法來配置使其在每種環境中都會選擇最合适的配置。
注解方式配置
在Spring中提供了@Profile注解,可以指定某個bean屬于哪個profile,使用的方式很簡單,隻需要在相應的方法上添加@Profile注釋并且在屬性中指定環境,例如@Profile("dev")這樣會告訴Spring這個bean隻有在dev的profile激活時才會被建立,@Profile注解可以用在類級别上,也可以用在方法級别上。隻有當規定的profile被激活時,相應的bean才會被建立,沒有指定profile的bean始終都會被建立于激活哪個profile無關。dev表示開發環境,prod表示生産環境,qa表示測試環境
XML方式配置
同樣在XML中也可以做配置,通過<beans>元素的profile屬性。我們可以在<beans>元素中嵌套<beans>元素,在一個XML檔案中建立多個profile環境
<beans profile="dev">
<bean id="dataSource" class="com.dataSource">
<value>....</value>
</bean>
</beans>
如何激活
指定了profile環境之後那麼該如何激活proflie呢?Spring在确定哪個profile處于激活狀态時,需要依賴兩個獨立的屬性,spring.profiles.active和spring.profiles.defaultz。active的值就是确定哪個profile是激活的,如果沒有設定active的值的話就會使用default的值,如果都沒有設定的話,那就沒有激活的profile,那就隻會建立那些沒有定義在profile中的bean。有多種方式來設定這兩個屬性
- 作為DispatcherServlet的初始化參數
- 作為Web應用的上下文參數
- 作為JND條目
- 作為環境變量
- 作為JVM的系統屬性
- 在內建測試類上使用@ActiveProfiles注解屬性
條件化的bean
在Spring4中引入了一個新的@Conditional注解,他可以用到帶有@Bean注解的方法上,如果給定的條件計算結果為true就會建立這個bean否則這個bean會被忽略。 假設現在有一個MagicBean的類,我們希望隻有設定了magic環境屬性的時候,Spring才會執行個體化這個類,如果環境中沒有這個屬性,那麼MagicBean将會被忽略。
/**
* 在@Conditional中給定一個class,他會指明條件
* 在@Conditional中是通過Condition接口進行對比的
* 也就是傳入@Conditional注解中的類必須要實作Condition接口
* 在Condition接口中有個matches方法用來比對條件
* 隻有這個方法傳回true才會建立這個bean這就是條件化的bean
*/
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean(){
return new MagicBean();
}
class MagicExistsCondition implements Condition{
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.containsProperty("magic");
//檢查環境中是否包含magic屬性,如果滿足就會傳回true
}
}
如上所示在magicBean方法上有@bean注解,同時也有@Conditional注解,表示這個bean需要指定條件才會被建立,那麼條件是什麼呢,具體的要求條件在@Conditional注解的參數中,這個參數要求是一個實作了Condition接口的類,實作這個接口需要實習其中的matches方法,這個方法才是真正傳回true或false的方法,用來判斷是否建立bean,當在matches中滿足條件時,他會傳回true,這樣注解@Conditional注解就會滿足條件,建立bean。在提一點,在matches方法中有兩個參數ConditionContext和AnnotatedTypeMetadata,這兩個都是接口,其中包含了很多的方法,借助他們可以實作更多的條件判斷。
處理自動裝配的歧義性
如果Spring中比對出多個适合的bean的話就會發生異常,這種自動裝配的歧義性該如何解決。現在描述一個場景,有一個Dessert接口,他表示甜品,并有三個類實作了這個接口,分别時Cake,Cookies和IceCream表示具體的甜品。
@Autowired
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
@Component
public class Cake implements Dessert{...}
@Component
public class Cookies implements Dessert{...}
@Component
public class IceCream implements Dessert{...}
這三個類都實作了@Component注解,在元件掃描的時候能夠發現他們并将他們建立為Spring應用上下文中的bean,當Spring試圖自動裝配Dessert參數時,他沒有唯一的無歧義的一個bean對應,Spring會抛出異常NoUniqueBeanDefinitionException;Spring有兩種方式可以解決歧義性
- 辨別首選
- 使用限定符
辨別首選很簡單,就是在你希望成為首選bean的帶有@Component注解的bean上添加@Primary注解就可以了,當Spring遇到同類型的bean時會自動選擇首選的bean。很明顯這種方法太過于簡陋,Spring提供了更加強大的限定符來限定條件。一個最簡單的栗子,如果你直接希望在Dessert中注入IceCream,那麼在隻需要在方法上添加注釋即可
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
@Qualifier中的參數就是要指定的bean的ID,更準确的說其實時bean的限定符,因為所有的bean都會有要給預設的限定符,于ID相同,是以這裡引用的時"iceCream"實際上引用的時限定符為"iceCream"的bean,也就是IceCream類将第一個字母小寫。除了使用預設的限定符,我們還可以自己指定自定義限定符,這樣就可以避免重構修改類名帶來的變化。
@Autowired
@Qualifier("cold")//使用限定符
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
@Component
@Qualifier("cold")//指定限定符
public class IceCream implements Dessert{...}
更加極端的情況是,我們使用這個描述特性的限定符的時候,如果兩個bean有同樣的特性呢,這個時候是不是需要在添加一個@Qualifier注解來表示更多的特性,但是這裡有一個小問題,在Java中不允許在同一個條目上重複出現相同類型的多個注解(Java8允許重複注解隻要這個注解本身帶有@Repeatable注解就可以,但是Spring的@Qualifier并沒有帶有這個注解,所有不可以重複)。這個時候我們需要自定義一個注解,在定義的時候添加@Qualifier注解,他們就具體有了 Qualifier特性。
@Target(ElementType.FIELD,ElementType.CONSTRUCTOR,EleentType.METHOD,ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy{}
就是這樣然後這個@Creamy就變成了一個注解了你可以在bean上使用@Creamy表示他是一個限定符,表示具體Creamy特性,同時你可以為另外的一個@Cold建立自定義注解,這樣這兩個注解都表示限定,而且具有不同的特性,最重要的時候你可以同時使用這兩個注解在同一個條目上。
bean的作用域
預設情況下,Spring應用上下文中的所有bean都是作為單例(singleton)建立的的,也就是一個bean不管被注入到其他的bean多少次,每次注入的都是同一個執行個體。單例的情況有好也有壞,是以在不同的情況下需要建立基于不同作用域的bean,Spring中定義了四種作用域
- 單例(Singleton)在整個應用中隻建立bean的一個執行個體
- 原型(Prototype)每次注入或者擷取的時候都會建立一個新的bean執行個體
- 會話(Session)在WEB應用中,為每一個會話建立一個bean執行個體
- 請求(Request)在WEB應用中,為每一個請求建立一個bean執行個體
對于前面兩種,Singleton是預設的不需要設定,設定Prototype非常簡單,你可以在類上或者方法上添加@Scope注解,@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)或者@Scope("protoype")前者更加安全不易出錯,這樣的話每次注入或者從Spring上下文中檢索該bean的時候都會建立新的執行個體。對于會話和請求作用域來說稍微有點複雜,先來描述一個場景。在WEB應用的電子商務應用中典型的場景是,可能會有一個bean代表使用者的購物車,如果這個購物車是單例的話那麼将會導緻所有的使用者都會向同一個購物車添加商品。如果購物車是原型的話,那麼在應用的一個地方添加商品,在應用的另一個地方就不能再用了因為将會是另一個原型作用域的購物車。最好的解決辦法就是使用會話作用域,他會告訴Spring為WEB應用的每個會話建立一個購物車,這會建立多個購物車執行個體,對于的給定的會話隻會建立一個執行個體,對于目前會話的相關操作來說,這個bean實際上是單例的。
@Component
@Scope(
value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.INTERFACES)
public shoppingCart cart(){...}
如上設定了session會話作用域,這裡重點要解釋一下proxyMode代理模式。假設我們需要将ShoppingCart bean注入到StoreService bean中去,因為 StoreService是單例的,當他建立的時候Spring試圖将ShoppingCart注入進去,但是他是會話作用域的,此時并不存在,直到某個使用者進入系統建立會話之後才會出現ShoppingCart執行個體。更重要的是系統在之後可能會出現多個ShoppingCart執行個體每個使用者一個,我們并不想讓Spring注入某個固定的ShoppingCart執行個體到StoreService中,我們希望StoreService處理購物車功能的時候他所使用的ShoppingCart執行個體恰好是目前會話對應的那個。Spring是如何解決的? Spring并不會将實際的ShoppingCart bean注入到StoreService中,Spring會注入一個ShoppingCart bean的代理如下圖所示。這個代理會暴露于于ShoppingCart相同的方法,是以StoreService會認為他是一個購物車,但是當StoreService調用ShoppingCart的方法時,代理會對其進行懶解析并調用委托給會話作用域内的真正ShoppingCart bean。在proxyMode屬性中我們設定為INTERFACES,這表明這個代理要實作ShoppingCart接口并将調用委托給實作bean。補充一點如果ShoppingCart是接口而不是類的話(這是最理想的代理模式),但他如果是具體類的話就必須使用CGLib來生成基于類的代理,這時候就需要将proxyMode屬性設定為TARGET_CLASS,以此來表示要以生成目标類擴充的方式建立代理。

運作時值注入
就目前來說我們對bean的設值都是在将值寫死在配置類中(也就是提前指定好值是什麼),有時候寫死是可以的,但有時候我們希望可以避免寫死能夠讓這些值在運作的時候在确定,為了實作這些功能,Spring提供了兩種在運作時求值的方式
- 屬性占位符
- Spring表達式語言(SpEL)
先看一下比較簡單的屬性占位符。一種方式是聲明屬性源并通過Spring的Environment來檢索屬性。可以通過@PropertySource("classpath:app.properties")來聲明屬性源
@Autowired
Environment env;
@Bean
public BlankDisc disc(){
return new BlankDisc(env.getProperty("disc.title"),env.getProperty("disc.artist"));
}
在本例中引入的配置檔案應該有這兩個屬性
disc.title = "title";
disc.artist = "artist";
這個屬性會加載到Environment中通過env取得,getProperty有四種重載形式。另外一種方式是Spring表達式語言,在Spring3中引入了SpEL,他能夠以一種強大和簡潔的方式将值裝配到bean屬性和構造器參數當中,在這個過程中使用表達式會在運作時計算得到值。SpEL具有以下特性
- 使用bean的ID來引用bean
- 調用方法和通路對象的屬性
- 對值進行算術、關系和邏輯運算
- 正規表達式
- 集合操作
最基本需要了解的是SpEL将表達式放在#{...}之中,放括号中可以放入很多類型,常量#{1},包括布爾類型字元串類型科學記數法等,Java對象#{T(System).currenTimeMills()},T()表達式會将java.lang.System視為Java中對應的類型,是以可以調用其靜态static修飾的currentTimeMills()方法,SpEL也可以引用其他的bean或其他bean的屬性#{sgtPeppers.artist},還可以引用系統對象#{systemProperties['disc.title']}。
總結
本章介紹一些進階的裝配技巧,根據不同環境激活不同的profile。解決自動裝配歧義性的辦法,首選項以及限定符。Spring bean的不同作用域,還有Spring語言表達式。