四、bean的作用域
默認(rèn)情況下,spring應(yīng)用上下文所有bean都是作為以單例的形式創(chuàng)建的。但是有時(shí)候,我們所使用的的類是易變的,因此重用是不安全的。spring定義了多種作用域,可以基于這些作用域創(chuàng)建bean,包括:
- 單例(
Singleton):在整個(gè)應(yīng)用中,只創(chuàng)建bean的一個(gè)實(shí)例 - 原型(
Prototype):每次注入或者通過Spring應(yīng)用上下文獲取的時(shí)候,都會(huì)創(chuàng)建一個(gè)新的bean實(shí)例。 - 會(huì)話(
Session):在Web應(yīng)用中,為每個(gè)會(huì)話創(chuàng)建一個(gè)bean實(shí)例。 - 請(qǐng)求(
Request):在Web應(yīng)用中,為每個(gè)請(qǐng)求創(chuàng)建一個(gè)bean實(shí)例。
單例是默認(rèn)的作用域,但是正如之前所述,對(duì)于易變類型,這并不合適,如果要選擇其他作用域,可以使用@Scope注解,可以和@Component或@Bean一起使用。
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {...}
說明:當(dāng)然我們可以這樣使用@Scope("prototype"),但是上面那樣更為安全且不易出錯(cuò)。如果要在XML中配置的話,可以這樣:
<bean id="notepad" class="com.myapp.Notepad" scope="prototype" />
說明:不管使用哪種方式來聲明原型作用域,每次注入或從spring上下文中檢索該bean的時(shí)候,都會(huì)創(chuàng)建新的實(shí)例,于是每次操作都有自己獨(dú)有的Notepad實(shí)例。
4.1 使用會(huì)話和請(qǐng)求作用域
在Web應(yīng)用中,如果能夠?qū)嵗跁?huì)話和請(qǐng)求范圍內(nèi)共享的bean,那將是非常有價(jià)值的事。比如在電商網(wǎng)站上購買商品,就希望購物車在此會(huì)話中是共享的:
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.INTERFACES)
public ShoppingCart cart(){}
說明:
上述作用域是會(huì)話,于是在一個(gè)購買商品的會(huì)話中,購物車是共享的。而
@Scope同時(shí)還有一個(gè)proxyMode屬性,其值為ScopedProxyMode.INTERFACES。這個(gè)屬性解決了將會(huì)話或請(qǐng)求作用域的bean注入到單例bean中所遇到的問題。假設(shè)要將
ShoppingCart bean注入到單例StoreService bean的Setter方法中:
public class StoreService {
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart){
this.shopingCart = shoppingCart;
}
}
因?yàn)?code>StoreService是一個(gè)單例的bean,會(huì)在Spring應(yīng)用上下文加載的時(shí)候創(chuàng)建,此時(shí),Spring會(huì)試圖將ShoppingCart bean注入到setShoppingCart()方法中去,但是ShoppingCart bean是會(huì)話作用域,此時(shí)并不存在(此時(shí)沒有發(fā)起會(huì)話)。同時(shí),系統(tǒng)中每次會(huì)話就有一個(gè)ShoppingCart實(shí)例,我們也并不希望注入某個(gè)固定的ShoppingCart實(shí)例到StoreService中。我們希望的是當(dāng)StoreService處理購物車功能時(shí),它所使用的ShoppingCart實(shí)例恰好是當(dāng)前會(huì)話所對(duì)應(yīng)的那一個(gè)。
-
實(shí)際上
Spring并不會(huì)將實(shí)際的ShoppingCart bean注入到StoreService中,而是會(huì)注入一個(gè)到ShoppingCart bean的代理,如圖所示。這個(gè)代理會(huì)暴露與ShoppingCart相同的方法,所以StoreService會(huì)認(rèn)為它就是一個(gè)購物車。但是,當(dāng)StoreService調(diào)用ShoppingCart的方法時(shí),代理會(huì)對(duì)其進(jìn)行懶解析并調(diào)用委托給會(huì)話作用域內(nèi)真正的ShoppingCart bean。
1 如果
ShoppingCart是接口的話,這是可以的。但是如果是一個(gè)具體的類,Spring就沒有辦法創(chuàng)建基于接口的代理了,需要使用CGLib來生成基于類的代理。所以,如果bean類型是具體類的話,必須要將proxyMode屬性設(shè)置為ScopedProxyMode.TARGET_CLASS,以此來標(biāo)明要以生成目標(biāo)擴(kuò)展的方式創(chuàng)建代理(而不是基于接口的代理擴(kuò)展)。
4.2 在 XML 中聲明作用域代理
在XML中要設(shè)置代理模式,需要使用Spring aop命名空間的一個(gè)新元素:
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
<aop:scoped-proxy />
</bean>
說明:這種命名方式和@Scope注解的proxyMode屬性功能相同的Spring XML配置元素。默認(rèn)情況下,它會(huì)使用CGLib創(chuàng)建目標(biāo)類的代理。但是我們也可以將proxy-target-class屬性設(shè)置為false,進(jìn)而要求它生成基于接口的代理:
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
<aop:scoped-proxy proxy-target-class="false" />
</bean>
當(dāng)然要使用這種配置,必須在頭部聲明:
xmln:aop="http://www.springframework.org/shema/aop"
五、運(yùn)行時(shí)值注入
前面也講過,有時(shí)候需要將具體的值注入。但是有時(shí)候硬編碼是可以的,但是有時(shí)候卻又希望這些值在運(yùn)行再確定。為了實(shí)現(xiàn)這些功能,Spring提供了兩種在運(yùn)行時(shí)求值的方式:
- 屬性占位符(
Property placeholder) -
Spring表達(dá)式語言(SpEL)
5.1 注入外部的值
在Spring中,處理外部值的最簡單方式就是聲明屬性源并通過Spring的Environment來檢索屬性。下面看一個(gè)例子:
package com.soundsystem;
import ...;
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")//聲明屬性源
public class ExpressiveConfig {
@Autowired
Environment env;
@Bean
public BleankDisc disc(){
return new BlankDisc(env.getProperty("disc.title"), env.getProperty("disc.artist"));//檢索屬性值
}
}
說明:在@PropertySource中配置了名為app.properties的文件:
disc.title=Sgt. Peppers Lonely Hearts Club Band
disc.artist=The Beatles
這個(gè)屬性文件會(huì)加載到Spring的Environment中,稍后可以從這里檢索屬性。于是就可以從中取得相關(guān)的 值了。
5.1.1 深入學(xué)習(xí) Spring 的 Environment
在Environment類中,上述的getProperty()方法并不是獲取屬性值的唯一方法,還有另外三個(gè):
String getProperty(String key);
String getProperty(String key, String defaultValue);
T getProperty(String key, Class<T> type);
T getProperty(String key, Class<T> type, T defaultValue);
使用第二個(gè)方法可以為相關(guān)屬性設(shè)置一個(gè)默認(rèn)值,如果在配置文件中取不到相關(guān)值,則可以使用默認(rèn)值:
@Bean
public BleankDisc disc(){
return new BlankDisc(env.getProperty("disc.title", "Rattle and Hum"),
env.getProperty("disc.artist", "U2"));//檢索屬性值
}
后面兩種方法與前面兩種類似,但是不會(huì)將所有的值都視為String類型。假如你想要獲取的值所代表的含義是連接池中所維持的連接數(shù)量。如果從屬性配置文件中得到的是一個(gè)String類型的值,那么在使用之前還要將其轉(zhuǎn)換為Integer類型:
int connectCount = env.getProperty("db.connection.count", Integer.class, 30);
Environment還提供了幾個(gè)與屬性相關(guān)的方法,如果在使用getProperty()方法時(shí)還沒有指定默認(rèn)值,并且這個(gè)屬性沒有定義,獲取到的值就是null。可以使用getRequiredProperty()方法指定某個(gè)屬性必須要定義:
@Bean
public BleankDisc disc(){
return new BlankDisc(env.getRequiredProperty("disc.title"),
env.getRequiredProperty("disc.artist");//檢索屬性值
}
如果上述兩個(gè)屬性值沒有定義的話將會(huì)拋出IllegalStateException異常。如果想檢查某個(gè)屬性是否存在的話可以:
boolean titleExists = env.containsProperty("disc.title");
最后,如果想將屬性解析為類的話,可以這樣:
Class<CompactDisc> cdClass = env.getPropertyAsClass("disc.class", CompactDisc.class);
除了上述的這些方法,Environment還提供了一些方法來檢查哪些profile處于激活狀態(tài):
String[] getActiveProfiles():返回激活profile名稱的數(shù)組
String[] getDefaultProfiles():返回默認(rèn)profile名稱的數(shù)組
boolean acceptsProfiles(String ... profiles):如果Environment支持給定的profile,返回true
5.1.2 解析屬性占位符
可以在XML中使用占位符按照如下方式解析BlankDisc構(gòu)造函數(shù)參數(shù):
<bean id="sgtPeppers" class="soundsystem.BlankDisc"
c:_title="${disc.title}"
c:_artist="${disc.artist}" />
稍后會(huì)討論這些屬性是如何解析的。如果我們依賴組件掃描和自動(dòng)裝配來創(chuàng)建和初始化應(yīng)用組件,那么就不需要指定占位符的配置文件或類了。
public BlankDisc(@Value("${disc.title}") String title, @Value("${disc.artist}") String artist){
this.title = title;
this.artist = artist;
}
為了使用占位符,必須配置一個(gè)PropertyPlaceholderConfigurer bean或PropertySourcePlaceholderConfigurer bean。從Spring 3.1開始,推薦使用后者,因?yàn)樗軌蚧?code>Spring Environment及其屬性源來解析占位符。
@Bean
public static PropertySourcePlaceholderConfigurer placeholderConfigurer(){
return new PropertySourcePlaceholderConfigurer();
}
而在XML文件應(yīng)這樣配置:
<context:property-palceholder />
當(dāng)然必須在頭中配置context的schema。以上就是兩種解析占位符的方式。
5.2 使用 Spring 表達(dá)式語言進(jìn)行裝配
在Spring表達(dá)式語言SpEL中有很多特性:
- 使用
bean的ID來引用bean; - 調(diào)用方法和訪問對(duì)象的屬性
- 對(duì)值進(jìn)行算術(shù)、關(guān)系和邏輯運(yùn)算
- 正則表達(dá)式匹配
- 集合操作
5.2.1 SpEL 樣例
需要了解的第一件事就是SpEL表達(dá)式要放到"#{...}"之中,這和屬性占位符類似。下面直接看例子:
#{1}
這就表示數(shù)字1。
#{T(System).currentTimeMillis()}
最終結(jié)果是當(dāng)前時(shí)刻的毫秒數(shù)。T()表示會(huì)將java.lang.System視為Java中對(duì)應(yīng)的類型,因此可以調(diào)用其static修飾的方法。我們還可以引用其他的bean或其他bean的屬性:
#{sgtPeppers.artist}
這表示引用ID為sgtPeppers bean的屬性。還可以通過systemProperties對(duì)象引用系統(tǒng)屬性:
#{systemProperties['disc.title']}
如果將之前配置注入相關(guān)屬性值應(yīng)用上SpEL表達(dá)式,則就是這樣:
public BlankDisc(@Value("#{systemProperties['disc.title']}") String title,
@Value("#{systemProperties['disc.title']}") String artist){
this.title = title;
this.artist = artist;
}
<bean id="sgtPeppers" class="soundsystem.BlankDisc"
c:_title="#{systemProperties['disc.title']}"
c:_artist="#{systemProperties['disc.title']}" />
5.2.2 表示字面值
#{3.14159}//浮點(diǎn)值
#{9.87E4}//科學(xué)計(jì)數(shù)法
#{'Hello'}//字符串
#{false}//布爾值
5.2.3 引用bean、屬性和方法
#{sgtPeppers}//使用bean ID作為SpEL表達(dá)式
#{sgtPeppers.artist}//引用bean的屬性artist
#{artistSelector.selectArtist()}//引用bean中的方法
#{artistSelector.selectArtist().toUpperCase()}//前一個(gè)方法返回String,則還可以繼續(xù)調(diào)用方法
#{artistSelector.selectArtist()?.toUpperCase()}//先判斷前一個(gè)方法是否返回String,如果是,則調(diào)用方法,否則返回null,不調(diào)用后續(xù)方法
5.2.4 在表達(dá)式中使用類型
如果要在SpEL中訪問類作用域的方法和常量的話,要依賴T()這個(gè)關(guān)鍵的運(yùn)算符:
#{T(java.lang.Math)}
#{T(java.lang.Math).PI}
#{T(java.lang.Math).random()}
5.2.5 SpEL運(yùn)算符
| 運(yùn)算符類型 | 運(yùn)算符 |
|---|---|
| 算術(shù)運(yùn)算 | +、-、*、/、%、^ |
| 比較運(yùn)算 | <、>、==、<=、>=、lt、gt、eq、le、ge |
| 邏輯運(yùn)算 | and、or、not、邏輯或 |
| 條件運(yùn)算 | ?:(ternary)、?:(Elvis) |
| 正則表達(dá)式 | matches |
5.2.6 計(jì)算正則表達(dá)式
#{admin.email matches '{[a-zA-Z0-9._%+-]+@[a-zA-Z0-9._]+\\.com}'}
5.2.7 計(jì)算集合
#{jukebox.songs[4].title}//引用集合中的一個(gè)元素
#{jukebox.songs[T(java.lang.Math).random() * jukebox.songs.size()].title}//隨機(jī)選取集合中的一個(gè)元素
#{'This is a test'[3]}//返回第四個(gè)位置的字符's'
5.2.8 查詢運(yùn)算符
#{jukebox.songs.?[artist eq 'Aerosmith']}//查詢名字為Aerosmith的歌曲,返回一個(gè)集合
#{jukebox.songs.^[artist eq 'Aerosmith']}//查詢集合中第一個(gè)artist屬性為Aerosmith的歌曲
#{jukebox.songs.$[artist eq 'Aerosmith']}//查詢集合中最后一個(gè)artist屬性為Aerosmith的歌曲
#{jukebox.songs.![title]}//不想要歌曲對(duì)象的集合,而是所有歌曲名稱的集合(沒有artist屬性的集合),返回一個(gè)新集合
注意:以上都是相關(guān)的值注入,而對(duì)于相關(guān)屬性的注入是不同的,spring是沒法自動(dòng)解析如Date這種類型的,所以我們需要編寫相關(guān)自定義的屬性編輯器,請(qǐng)參看前面小節(jié):《2.spring的IOC容器注入(spring筆記)》