文章作者:Tyan
博客:noahsnail.com
3.5 Bean的作用域
????????當(dāng)你創(chuàng)建bean定義時憎瘸,你創(chuàng)建了一個配方用于創(chuàng)建bean定義中定義的類的實例勋磕。bean定義是配方的想法是很重要的,因為這意味著對于一個類,你可以根據(jù)一個配方創(chuàng)建許多對象實例肮帐。
????????你不僅能管理要插入對象中的的各種依賴和配置值,而且能管理對象的作用域鞭呕,對象是從特定的bean定義中創(chuàng)建的文捶。這種方法是強大且靈活的,你可以通過配置文件選擇你創(chuàng)建的對象的作用域道伟,從而代替Java類級別對象的內(nèi)置作用域迹缀。定義的beans將部署成多種作用域中的一種:開箱即用,Spring框架支持六種作用域蜜徽,如果你使用感知web的ApplicationContext
祝懂,你只可以使用其中的五種作用域。
????????下面的作用域支持開箱即用拘鞋。你也可以創(chuàng)建一個定制的作用域砚蓬。
表 3.3 bean作用域
作用域 | 描述 |
---|---|
singleton | (默認) 每個Spring IoC容器使單個bean定義只能創(chuàng)建一個對象實例。 |
prototype | 單個bean定義可以創(chuàng)建任何數(shù)量的對象實例掐禁。 |
request | 單個bean定義的創(chuàng)建實例的作用域為單個HTTP request的聲明周期怜械;也就是說,每個HTTP request有它自己的根據(jù)bean定義創(chuàng)建的實例傅事。只在感知Spring ApplicationContext 的上下文中有效缕允。 |
session | 單個bean定義的創(chuàng)建實例的作用域為HTTP Session 的生命周期. 只在感知Spring ApplicationContext 的上下文中有效。 |
application | 單個bean定義的創(chuàng)建實例的作用域為ServletContext 的生命周期蹭越。 只在感知Spring ApplicationContext 的上下文中有效障本。 |
websocket | 單個bean定義的創(chuàng)建實例的作用域為WebSocket 的生命周期。 只在感知Spring ApplicationContext 的上下文中有效。 |
從Spring 3.0驾霜,引入了
thread scope
作用域案训,但默認情況下是不注冊的。更多的信息請看SimpleThreadScope
文檔粪糙。關(guān)于怎么注冊thread scope
作用域或任何其它的定制作用域的介紹强霎,請看『Using a custom scope』小節(jié)。
3.5.1 單例作用域
????????單例bean只管理一個共享實例蓉冈,id匹配bean定義的所有對beans的請求城舞,Spring容器會返回一個特定的bean實例。
????????換言之寞酿,當(dāng)你定義一個bean定義時家夺,它的作用域為單例,Spring IoC容器會根據(jù)bean定義創(chuàng)建一個確定的對象實例伐弹。這個單獨的實例存儲在單例beans的緩存中拉馋,接下來的對這個命名bean的所有請求和引用都會返回那個緩存的對象。
????????Spring中的單例bean概念不同于《設(shè)計模式》書中定義的單例模式惨好。設(shè)計模式中的單例是對對象的作用域進行硬編碼煌茴,為的是每個類加載器只能創(chuàng)建一個特定類的實例。Spring單例作用域最好的描述是每個容器每個類昧狮。這意味著如果你在單個的Spring容器中為一個特定的類定義了一個bean景馁,Spring只會根據(jù)bean定義創(chuàng)建一個類的實例。在Spring中單例作用域是默認的作用域逗鸣。為了在XML定義一個單例bean,你可以像下面一樣寫绰精,例如:
<bean id="accountService" class="com.foo.DefaultAccountService"/>
<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>
3.5.2 原型作用域
????????非單例模式撒璧,bean部署采用原型作用域時,每次產(chǎn)生一個特定bean的請求時都會創(chuàng)建一個新的bean實例笨使。也就是說卿樱,這個bean會注入到另一個bean中或你可以在容器中通過調(diào)用getBean()
方法來請求它。通常硫椰,對于所有有狀態(tài)的beans使用原型作用域繁调,對于無狀態(tài)的beans使用單例作用域。
????????下面的圖闡述了Spring原型作用域靶草。數(shù)據(jù)訪問對象(DAO)通常是不會配置為原型的蹄胰,因為一個典型的DAO不會有任何會話狀態(tài);對于作者來說很容易重用單例圖的核心奕翔。
????????下面的例子在XML中定義一個原型bean:
<bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/>
????????與其它作用域相比裕寨,Spring不管理原型bean的完整生命周期:容器初始化、配置,另外組裝原型對象宾袜,并把它傳遞給客戶端捻艳,之后不再記錄原型實例。因此庆猫,雖然不管什么作用域初始化生命周期回調(diào)函數(shù)都會在所有對象上調(diào)用认轨,但是在原型作用域的情況下,不會調(diào)用配置的銷毀生命周期回調(diào)函數(shù)月培∴易郑客戶端代碼必須清理原型作用域的對象并釋放原型bean擁有的昂貴資源。為了使Spring容器釋放原型bean擁有的資源节视,嘗試使用定制的bean后處理程序拳锚,它擁有需要清理的bean的引用。
????????在有些方面寻行,關(guān)于原型作用域霍掺,Spring容器的角色像是Java中new
操作符的替代品。所有生命周期的管理必須由客戶端處理拌蜘。(Spring容器中更多關(guān)于bean生命周期的細節(jié)杆烁,請看3.6.1小節(jié),"生命周期回調(diào)")简卧。
3.5.3 含有原型bean依賴的單例bean
????????當(dāng)你使用含有原型bean依賴的單例作用域bean時兔魂,要意識到依賴解析是在實例化時。因此如果你使用依賴注入將原型作用域的bean注入到單例作用域的bean中時举娩,將會實例化一個新的原型bean并依賴注入到單例bean中析校。原型bean實例曾經(jīng)是唯一提供給單例作用域的bean的實例。
????????假設(shè)你想在運行時讓單例作用域的bean重復(fù)的獲得原型作用域bean的新實例铜涉。你不能依賴注入原型作用域的bean到你的單例bean中智玻,因為當(dāng)Spring容器實例化單例bean,解析并注入它的依賴時芙代,注入只發(fā)生一次吊奢。如果你在運行時不止一次需要原型bean的實例,請看3.4.6小節(jié)纹烹,"方法注入"页滚。
3.5.4 Request、session铺呵、application和 WebSocket作用域
????????如果你使用感知web的Spring ApplicationContext
實現(xiàn)(例如XmlWebApplicationContext
)裹驰,request
,session
陪蜻,application
和websocket
作用域是唯一可用的作用域邦马。如果你通過正規(guī)的Spring IoC容器例如ClassPathXmlApplicationContext
來使用這些作用域,會拋出IllegalStateException
異常,投訴使用了一個未知的bean作用域滋将。
web配置初始化
????????為了支持request
邻悬,session
,application
和websocket
標(biāo)準(zhǔn)的bean作用域随闽,在你定義你的bean之前需要進行一些較小的初始化配置父丰。(對于標(biāo)準(zhǔn)作用域singleton
和prototype
,初始化步驟不需要的掘宪。)
????????如果你使用Servlet 2.5的web容器蛾扇,在Spring的DispatcherServlet
之外處理請求(例如使用JSF或Struts時),你需要注冊org.springframework.web.context.request.RequestContextListener
ServletRequestListener
魏滚。對于Servlet 3.0+镀首,能通過WebApplicationInitializer
接口以編程方式處理。對于更早的容器鼠次,可以在應(yīng)用程序的web.xml
文件中添加下面的聲明來代替:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>
????????如果你的監(jiān)聽器設(shè)置有問題更哄,作為一種選擇,你可以考慮Spring的RequestContextFilter
腥寇。過濾器映射依賴于web應(yīng)用程序的相關(guān)配置成翩,因此你必須適當(dāng)?shù)母乃?/p>
<web-app>
...
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
????????DispatcherServlet
赦役,RequestContextListener
和RequestContextFilter
都是在做同樣的事麻敌,也就是說將HTTP請求對象綁定到服務(wù)請求的Thread
上。這使得request作用域和session作用域的beans在更深一層的調(diào)用鏈中是可用的掂摔。
Request作用域
考慮下面的bean定義的XML配置:
<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>
????????對于每一個HTTP請求术羔,Spring容器通過使用loginAction
定義創(chuàng)建一個新的LoginAction
bean實例。也就是說乙漓,loginAction
bean的作用域是在HTTP請求級別的聂示。你可以任意改變創(chuàng)建的實例的內(nèi)部狀態(tài),因為其它的根據(jù)loginAction
bean定義創(chuàng)建的實例不會看到這些狀態(tài)的改變簇秒;它們對于每個單獨的請求都是獨有的。當(dāng)請求處理完成時秀鞭,請求作用域的bean被丟棄趋观。
????????當(dāng)使用注解驅(qū)動的組件或Java配置時,@RequestScope
注解能用來指定一個組件的作用域為request锋边。
@RequestScope
@Component
public class LoginAction {
// ...
}
Session作用域
????????考慮下面的bean定義的XML配置:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
????????對于單個HTTP Session的生命周期皱坛,Spring容器通過userPreferences
bean定義創(chuàng)建一個UserPreferences
bean實例。換句話說豆巨,userPreferences
bean的有效作用域是HTTP Session級別的剩辟。正如request作用域的beans一樣,你可以任意改變你想改變的創(chuàng)建的bean實例的內(nèi)部狀態(tài),知道其它的使用根據(jù)userPreferences
bean定義創(chuàng)建的HTTP Session實例也不會看到這些內(nèi)部狀態(tài)的改變贩猎,因為它們對于每個單獨的HTTP Session都是獨有的熊户。當(dāng)HTTP Session被最終銷毀時,Session作用域的bean也被銷毀吭服。
????????當(dāng)使用注解驅(qū)動的組件或Java配置時嚷堡,@SessionScope
注解能用來指定一個組件的作用域為session。
@SessionScope
@Component
public class UserPreferences {
// ...
}
Application作用域
????????考慮下面的bean定義的XML配置:
<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>
????????對于整個web應(yīng)用而言艇棕,Spring容器根據(jù)appPreferences
bean定義只創(chuàng)建一次AppPreferences
bean的新實例蝌戒。也就是說,appPreferences
bean的作用域是ServletContext
級別的沼琉,作為一個正規(guī)的ServletContext
特性來存儲北苟。這有點類似于Spring的單例bean,但在兩個方面是不同的:它對于每個ServletContext
是單例的打瘪,而不是每個Spring ApplicationContext
(在任何給定的web應(yīng)用中可能有幾個ApplicationContext
)友鼻,它是真正顯露的,因此作為一個ServletContext
特性是可見的瑟慈。
????????當(dāng)使用注解驅(qū)動的組件或Java配置時桃移,@ApplicationScope
注解能用來指定一個組件的作用域為Application。
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
具有作用域的bean作為依賴項
????????Spring IoC容器不僅管理對象的實例化葛碧,而且管理協(xié)作者(或依賴)的綁定借杰。例如,如果你想將一個具有HTTP request作用域的bean注入到另一個具有更長生命周期作用域的bean中进泼,你可能選擇注入一個AOP代理來代替具有作用域的bean蔗衡。也就是說,你需要注入一個代理對象乳绕,這個對象能顯露與具有作用域的對象相同的接口绞惦,但也能從相關(guān)的作用域中(例如HTTP request作用域)得到真正的目標(biāo)對象,能通過委派方法調(diào)用到真正的對象洋措。
你也可以在作用域為
singleton
的beans之間使用<aop:scoped-proxy/>
济蝉,將通過中間代理的引用進行序列化,因此能通過反序列化重新獲得目標(biāo)的單例bean菠发。
當(dāng)將作用域為
prototype
的bean聲明為<aop:scoped-proxy/>
時王滤,每個在共享代理上的方法調(diào)用會引起一個新目標(biāo)實例(調(diào)用朝向的)的創(chuàng)建。
通過生命周期安全的方式訪問更短的作用域中beans滓鸠,作用域代理也不是唯一的方式雁乡。你也可以簡單的聲明你的注入點(例如,構(gòu)造函數(shù)/setter參數(shù)或自動裝配領(lǐng)域)為
ObjectFactory<MyTargetBean>
糜俗,考慮到每次需要的時候通過getObject()
調(diào)用來取得索要的當(dāng)前實例——沒有分別控制實例或儲存它踱稍。
JSR-300變量被稱作
Provider
曲饱,對于每一次取回嘗試使用Provider<MyTargetBean>
聲明和對應(yīng)的get()
調(diào)用。關(guān)于JSR-330整體的更多細節(jié)請看這兒珠月。
????????下面例子中的配置只有一行扩淀,但對于理解它背后的"why"和"how"是重要的。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- an HTTP Session-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
<!-- instructs the container to proxy the surrounding bean -->
<aop:scoped-proxy/>
</bean>
<!-- a singleton-scoped bean injected with a proxy to the above bean -->
<bean id="userService" class="com.foo.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>
????????為了創(chuàng)建這樣一個代理桥温,你插入一個子元素<aop:scoped-proxy/>
到具有作用域的bean定義中(看"選擇創(chuàng)建的代理類型"小節(jié)和38章引矩,基于XML Schema的配置)。為什么bean定義的作用域為request
侵浸,session
和定制作用域級別需要<aop:scoped-proxy/>
元素旺韭?讓我們檢查下面的單例bean定義,并將它與你需要定義的前面提到的作用域進行比較(注意下面的userPreferences
bean定義按目前情況是不完全的)掏觉。
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
<bean id="userManager" class="com.foo.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
????????在上面的例子中区端,單例bean userManager
通過引用被注入到具有HTTP Session
作用域的bean userPreferences
中。這的突出點是userManager
bean是單例:每個容器它將確定的被實例化一次澳腹,它的依賴(在這個例子中只有一個织盼,userPreferences
bean)也只注入一次。這意味著userManager
bean只能對確定的同一個userPreferences
對象進行操作酱塔,也就是最初注入的那個對象沥邻。
????????當(dāng)將一個短期作用域的bean注入到一個長期作用域的bean中時,這不是你想要的行為羊娃,例如將一個具有HTTP Session
作用域的協(xié)作bean作為一個依賴注入到一個單例bean中唐全。當(dāng)然,你需要一個單一的userManager
對象蕊玷,對于HTTP Session
的生命周期邮利,你需要一個特定的被稱為HTTP Session
的userPreferences
對象。因此容器創(chuàng)建了一個與UserPreferences
類暴露相同的公共接口的對象(理想情況下是一個UserPreferences
實例)垃帅,這個對象能從作用域機制中(HTTP request延届,Session等)取得真正的UserPreferences
對象。容器將這個代理對象注入到userManager
bean中贸诚,userManager
bean不會意識到UserPreferences
引用是一個代理方庭。在這個例子中,當(dāng)UserManager
實例調(diào)用依賴注入的UserPreferences
對象的方法時酱固,它實際上調(diào)用的是代理中的一個方法二鳄。代理能從HTTP Session
中(在這個例子)取得真正的UserPreferences
對象,將方法調(diào)用委托到取得的真正的UserPreferences
對象上媒怯。
????????因此當(dāng)注入具有request或session作用域的bean到協(xié)作對象中時,你需要下面的髓窜,正確的扇苞,完整的配置:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
<aop:scoped-proxy/>
</bean>
<bean id="userManager" class="com.foo.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
選擇創(chuàng)建的代理類型
????????當(dāng)Spring容器為具有<aop:scoped-proxy/>
標(biāo)記的bean創(chuàng)建代理時欺殿,默認情況下,創(chuàng)建一個基于CGLIB的類代理鳖敷。
CGLIB代理只攔截公有方法調(diào)用脖苏。在這個代理上不調(diào)用非公有方法;它們不能委托給實際作用域目標(biāo)對象定踱。
????????作為一種選擇棍潘,對于這種具有作用域的bean你可以配置Spring容器創(chuàng)建標(biāo)準(zhǔn)JDK基于接口的代理,通過指定<aop:scoped-proxy/>
元素的proxy-target-class
特定的值為false
崖媚。使用JDK基于接口的代理意味著在你應(yīng)用程序類路徑中你不需要額外的庫來支持這種代理的使用亦歉。然而,它也意味著具有作用域的bean的類必須實現(xiàn)至少一個接口畅哑,并且注入這個bean的所有協(xié)作者必須通過它接口中的一個來引用它肴楷。
<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.foo.DefaultUserPreferences" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
<bean id="userManager" class="com.foo.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
????????關(guān)于選擇基于類或基于接口代理的更多細節(jié)信心,請看7.6小節(jié)荠呐,"代理機制"赛蔫。
3.5.5 定制作用域
????????bean作用域機制是可擴展的;你可以定義你自己的作用域泥张,甚至重新定義現(xiàn)有的作用域呵恢,雖然后者被認為是一種不好的實踐,你不能覆蓋內(nèi)置的singleton
作用域和prototype
作用域媚创。
創(chuàng)建一個定制作用域
????????為了將你的定制作用域集成到Spring容器中渗钉,你需要實現(xiàn)org.springframework.beans.factory.config.Scope
接口,這一節(jié)將描述這個接口筝野。對于怎樣實現(xiàn)你自己作用域的想法晌姚,請看Spring框架本身提供的Scope
實現(xiàn)和Scope
文檔,它們解釋了你需要實現(xiàn)的方法的更多細節(jié)歇竟。
????????Scope
接口有四個方法挥唠,從作用域中取得對象,從作用域中移除對象焕议,并且允許它們被銷毀宝磨。
????????下面的方法從潛在的作用域返回對象。session作用域?qū)崿F(xiàn)盅安,例如唤锉,返回具有session作用域的bean(如果它不存在,這個方法返回一個bean的新實例别瞭,然后綁定到session中準(zhǔn)備將來引用)窿祥。
Object get(String name, ObjectFactory objectFactory)
????????下面的方法從潛在作用域中移除對象。以session作用域?qū)崿F(xiàn)為例蝙寨,從潛在的session中移除session作用域的bean晒衩。對象應(yīng)該被返回嗤瞎,但如果沒有找到指定名字的對象會返回空。
Object remove(String name)
????????下面的方法是注冊當(dāng)作用域銷毀時或當(dāng)作用域中的指定對象銷毀時听系,作用域應(yīng)該執(zhí)行的回調(diào)函數(shù)贝奇。銷毀回調(diào)函數(shù)的更多信息請看文檔或Spring作用域?qū)崿F(xiàn)。
void registerDestructionCallback(String name, Runnable destructionCallback)
????????下面的方法是獲得潛在作用域的會話標(biāo)識符靠胜。每個作用域的標(biāo)識符都是不同的掉瞳。對于session作用域?qū)崿F(xiàn),標(biāo)識符是session標(biāo)識符浪漠。
String getConversationId()
使用定制作用域
????????在你編寫和測試一個或多個定制Scope
實現(xiàn)之后陕习,你需要讓Spring容器感知到你的新作用域。下面是在Spring容器中注冊一個新Scope
的主要方法:
void registerScope(String scopeName, Scope scope);
????????這個方法是在ConfigurableBeanFactory
接口中聲明的郑藏,在大多數(shù)具體的ApplicationContext
實現(xiàn)中都可獲得衡查,在Spring中通過BeanFactory
屬性得到。
????????registerScope(..)
方法中的第一個參數(shù)是關(guān)于作用域的唯一名字必盖;Spring容器本身中的這種名字的例子是singleton
和prototype
拌牲。registerScope(..)
方法中的第二個參數(shù)是你想注冊和使用的定制Scope
實現(xiàn)的真正實例。
????????假設(shè)你編寫了你的定制Scope
實現(xiàn)并按如下注冊歌粥。
下面的例子使用Spring包含的
SimpleThreadScope
塌忽,但默認是不注冊的。這個用法說明與你自己的定制Scope
是一樣的失驶。
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
????????然后創(chuàng)建具有你自己定制的Scope
規(guī)則的bean定義:
<bean id="..." class="..." scope="thread">
????????在定制Scope
實現(xiàn)后土居,你不會受限于作用域的程序注冊。你也可以聲明式的進行Scope
注冊嬉探,使用CustomScopeConfigurer
類:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread">
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
<bean id="bar" class="x.y.Bar" scope="thread">
<property name="name" value="Rick"/>
<aop:scoped-proxy/>
</bean>
<bean id="foo" class="x.y.Foo">
<property name="bar" ref="bar"/>
</bean>
</beans>
當(dāng)你在
FactoryBean
實現(xiàn)中放入<aop:scoped-proxy/>
時擦耀,它是工廠bean本身具有作用域,不是從getObject()
中返回的對象涩堤。