原文 :一文讀懂Spring Bean作用域 - RelaxHeart網(wǎng)
Spring Bean的幾種作用域
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è)實(shí)例。
單例作用域(Singleton)
默認(rèn)情況下Spring應(yīng)用上下文的Bean都是以單例(singleton)形式創(chuàng)建的,即不管一個(gè)bean被注入到其他bean多少次,每次注入的都是同一個(gè)實(shí)例。
多數(shù)情況下,單例Bean是最理想的方案。但是有時(shí)候我們使用的類是亦變的,他們會(huì)保持一些狀態(tài),因此重用是不安全的。這時(shí)候我們就需要結(jié)合具體的業(yè)務(wù)場(chǎng)景針對(duì)性的創(chuàng)建非singleton類型的Bean實(shí)例。
單例是默認(rèn)的作用域,但是對(duì)于哪些易變的類型這個(gè)并不適合。如果選擇其他作用域,要使用@Scope注解,它可以與@Component或@Bean一起使用。
原型作用域(Prototype)
比如,我們使用組件掃描來發(fā)現(xiàn)和聲明bean,那么我們可以在bean的類上使用@Scope注解,將其聲明為原型bean:
/**
* @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
* @Date: 2019-7-6 0006 14:47
* @Description: 無描述信息
*/
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ProtoTypeBean {
}
這里使用ConfigurableBeanFactory類的SCOPE_PROTOTYPE常量設(shè)置了原型作用域。我們也可以使用@Scope("prototype")更簡(jiǎn)潔的方式,個(gè)人還是習(xí)慣用第一種使用SCOPE_PROTOTYPE常量的方式,更安全且不易出錯(cuò)。
如果想在Java配置中將ProtoTypeBean 聲明為原型bean,可以使用組合@Scope和@Bean里指定所需要的作用域:
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public ProtoTypeBean instance(){
return new ProtoTypeBean();
}
同樣,如果我們使用XML配置Bean,可以使用<bean>元素的scope屬性來設(shè)置作用域:
<bean id = "baseService" class = "cn.relaxheart.service.BaseServiceImpl" scope="prototype"/>
不論使用那種方式來聲明原型作用域,每次注入或者從spring應(yīng)用上下文中檢索該bean時(shí)候,都會(huì)創(chuàng)建新的實(shí)例。
使用會(huì)話(Session)和請(qǐng)求(Request)作用域
在Web應(yīng)用中,能夠?qū)嵗谠跁?huì)話和請(qǐng)求范圍內(nèi)的Bean是很有價(jià)值的。
例如在電商系統(tǒng)中,可能會(huì)有一個(gè)Bean代表的是用戶的購(gòu)物車(ShoppingCart),如果ShoppingCart是單例的話,那將導(dǎo)致所有的用戶往同一個(gè)購(gòu)物車中添加商品;另外如果購(gòu)物車是原型類型,那么用戶在應(yīng)用的某個(gè)地方A添加商品,而在另一個(gè)地方B可能就不可用了,因?yàn)槊看巫⑷氲亩紝⑹且粋€(gè)新的實(shí)例。
所以就購(gòu)物車的Bean來看,會(huì)話作用域是最合適的了,因?yàn)樗c給定的用戶的關(guān)聯(lián)度最大,如果要在Java配之類使用會(huì)話作用域那么跟原型作用域用法一樣,我們使用@Scope注解實(shí)現(xiàn):
/**
* @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
* @Date: 2019-7-6 0006 16:03
* @Description: 無描述信息
*/
@Configuration
public class AppConfiguration {
@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.INTERFACES)
public ShoppingCart cart(){ … }
}
這里將@Scope的value設(shè)置為WebApplicationContext.SCOPE_SESSION即告訴Spring為Web的每個(gè)會(huì)話只創(chuàng)建一個(gè)ShoppingCart 實(shí)例。這樣是會(huì)創(chuàng)建很多個(gè)實(shí)例,但是對(duì)于同一個(gè)會(huì)話只有一個(gè)。在當(dāng)前會(huì)話中,這個(gè)Bean相當(dāng)于是一個(gè)單例。
上面@Scope注解中我們有用到除value外的另一個(gè)屬性proxyMode ,這里它被設(shè)置為ScopedProxyMode.INTERFACES(接口代理)。這個(gè)屬性解決了將會(huì)話或者請(qǐng)求作用域的Bean注入到單例bean所遇到的問題。在描述proxyMode屬性之前,我們先來看下proxyMode解決的問題場(chǎng)景:
/**
* @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
* @Date: 2019-7-6 0006 16:03
* @Description: 無描述信息
*/
@Component
public class StoreService {
private ShoppingCart cart;
@Autowired
public void setShoppingCart(ShoppingCart cart){
this.cart = cart;
}
}
這里的StoreService 是一個(gè)單例的bean(默認(rèn)情況下),會(huì)在Spring應(yīng)用上下文加載的時(shí)候創(chuàng)建,當(dāng)它創(chuàng)建的時(shí)候,Spring會(huì)試圖將ShoppingCart 的bean實(shí)例注入到setShoppingCart()方法中,但是ShoppingCart bean是會(huì)話作用域的,此時(shí)并不存在。直到某個(gè)用戶進(jìn)入系統(tǒng),創(chuàng)建了會(huì)話之后,才會(huì)出現(xiàn)ShoppingCart 的實(shí)例。所以這個(gè)時(shí)候注入會(huì)報(bào)錯(cuò)找不到ShoppingCart 的 Bean定義。
另外,系統(tǒng)中將會(huì)有多個(gè)ShoppingCart實(shí)例(每個(gè)用戶創(chuàng)建一個(gè)會(huì)話時(shí)會(huì)創(chuàng)建一個(gè))。我們并不想讓Spring注入某一個(gè)固定的ShoppingCart 實(shí)例到StoreService。我們希望的是放StoreService處理購(gòu)物車功能時(shí),它所使用的ShoppingCart 實(shí)例恰好是當(dāng)前用戶會(huì)話所對(duì)應(yīng)的那個(gè)ShoppingCart實(shí)例。
這個(gè)問題要怎么解決呢?
Spring并不會(huì)將實(shí)際的ShoppingCart bean注入到StoreService中,但是Spring會(huì)注入一個(gè)到ShoppingCart bean的代理,如下圖所示:

結(jié)合上圖看,這個(gè)代理會(huì)暴露于ShoppingCart相同的方法,所以StoreService會(huì)認(rèn)為它就是一個(gè)購(gòu)物車。但是,當(dāng)StoreService去調(diào)用ShoppingCart 的方法時(shí),代理會(huì)對(duì)其進(jìn)行解析并將調(diào)用委托給會(huì)話作用域內(nèi)真正的ShoppingCart bean。
然后,咱們帶著對(duì)這個(gè)作用域的理解,在回到上述proxyMode屬性上,如我們配置所示:
proxyMode = ScopedProxyMode.INTERFACES
這里表明這個(gè)代理要實(shí)現(xiàn)ShoppingCart 接口,并將調(diào)用委托給實(shí)現(xiàn)bean。
這里的ScopedProxyMode枚舉一種包含4中類型:
public enum ScopedProxyMode {
/**
* Default typically equals {@link #NO}, unless a different default
* has been configured at the component-scan instruction level.
*/
DEFAULT,
/**
* Do not create a scoped proxy.
* <p>This proxy-mode is not typically useful when used with a
* non-singleton scoped instance, which should favor the use of the
* {@link #INTERFACES} or {@link #TARGET_CLASS} proxy-modes instead if it
* is to be used as a dependency.
*/
NO,
/**
* Create a JDK dynamic proxy implementing <i>all</i> interfaces exposed by
* the class of the target object.
*/
INTERFACES,
/**
* Create a class-based proxy (uses CGLIB).
*/
TARGET_CLASS;
}
關(guān)于代理的知識(shí)這里不細(xì)說了,我們主要看下INTERFACES與TARGET_CLASS這兩個(gè)代理類型的使用: 顯然INTERFACES從字面上來看是“接口”的意思,即基于接口實(shí)現(xiàn)的代理,也是最理想的代理模式,因?yàn)槲覀冞@里注入的ShoppingCart是接口類型所以使用INTERFACES;那如果要注入的Bean是一個(gè)具體的類,Spring就沒有辦法創(chuàng)建基于接口的代理了。這個(gè)時(shí)候它必須使用CGLib來生成基于類的代理。所以如果bean類型是具體類的話,我們必須使用TARGET_CLASS,以此來表明要以生成目標(biāo)類擴(kuò)展的方式創(chuàng)建代理。
這里請(qǐng)求作用域也同樣面臨這類裝配的問題。處理方式是一樣的:bean應(yīng)該以作用域代理的方式進(jìn)行注入。
在XML中聲明作用域代理
上述關(guān)于session、request級(jí)作用域代理注入方式我們是以JavaConfig的方式實(shí)現(xiàn)的,在XML中應(yīng)該怎么搞呢。
XML中配置作用域代理需要引入Spring AOP命名空間的一個(gè)新元素:
<bean id = "cart" class="cn.relaxheart.service.ShoppingCart" scope="session">
<!-- 就是它了 -->
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
<aop:scoped-proxy >是與@Scope的proxyMode屬性作用相同的XML配置元素,會(huì)告訴Spring創(chuàng)建一個(gè)作用域代理。默認(rèn)情況下,他會(huì)使用CGLib創(chuàng)建目標(biāo)類的代理。但是我們這里需要的是基于接口的代理,只需將 proxy-target-class屬性值設(shè)置為false就可以了。
總結(jié)
(1)Spring Bean的4種作用域:singleton、Prototype、Session、Request
(2)基于Java配置實(shí)現(xiàn)作用域控制主要的注解:@Scope
(3)基于XML配置實(shí)現(xiàn)作用域控制:<bean id = "cart" class="cn.relaxheart.service.ShoppingCart" scope="session"/>
(4)將會(huì)話/請(qǐng)求作用域Bean注入單例bean需要以作用域代理的方式注入,主要分兩類:接口代理 (INTERFACES)& 類代理(TARGET_CLASS),代理配置對(duì)應(yīng)的屬性為proxyMode