3.4. Bean的作用域
創建一個bean定義,其實質是用該bean定義對應的類來創建真正實例的“配方(recipe)”。把bean定義看成一個配方很有意義,它與class很類似,只根據一張“處方”就可以創建多個實例。
你不僅可以控制注入到對象中的各種依賴和配置值,還可以控制該對象的作用域。這樣你可以靈活選擇所建對象的作用域,而不必在Java Class級定義作用域。Spring Framework支持五種作用域(其中有三種只能用在基於web的Spring ApplicationContext)。
內置支持的作用域分列如下:
Table 3.4. Bean作用域
作用域 | 描述 |
---|---|
在每個Spring IoC容器中一個bean定義對應一個對象實例。 |
|
一個bean定義對應多個對象實例。 |
|
在一次HTTP請求中,一個bean定義對應一個實例;即每次HTTP請求將會有各自的bean實例, 它們依據某個bean定義創建而成。該作用域僅在基於web的Spring ApplicationContext情形下有效。 |
|
在一個HTTP Session中,一個bean定義對應一個實例。該作用域僅在基於web的Spring ApplicationContext情形下有效。 |
|
在一個全局的HTTP Session中,一個bean定義對應一個實例。典型情況下,僅在使用portlet context的時候有效。該作用域僅在基於web的Spring ApplicationContext情形下有效。 |
當一個bean的作用域爲singleton, 那麼Spring IoC容器中只會存在一個共享的bean實例,並且所有對bean的請求,只要id與該bean定義相匹配,則只會返回bean的同一實例。
換言之,當把一個bean定義設置爲singlton作用域時,Spring IoC容器只會創建該bean定義的唯一實例。這個單一實例會被存儲到單例緩存(singleton cache)中,並且所有針對該bean的後續請求和引用都將返回被緩存的對象實例。
請注意Spring的singleton bean概念與“四人幫”(GoF)模式一書中定義的Singleton模式是完全不同的。經典的GoF Singleton模式中所謂的對象範圍是指在每一個ClassLoader中指定class創建的實例有且僅有一個。把Spring的singleton作用域描述成一個container對應一個bean實例最爲貼切。亦即,假如在單個Spring容器內定義了某個指定class的bean,那麼Spring容器將會創建一個且僅有一個由該bean定義指定的類實例。Singleton作用域是Spring中的缺省作用域。要在XML中將bean定義成singleton,可以這樣配置:
<bean id="accountService" class="com.foo.DefaultAccountService"/> <!-- the following is equivalent, though redundant (singleton scope is the default); using spring-beans-2.0.dtd --> <bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/> <!-- the following is equivalent and preserved for backward compatibility in spring-beans.dtd --> <bean id="accountService" class="com.foo.DefaultAccountService" singleton="true"/>
Prototype作用域的bean會導致在每次對該bean請求(將其注入到另一個bean中,或者以程序的方式調用容器的getBean()方法)時都會創建一個新的bean實例。根據經驗,對有狀態的bean應該使用prototype作用域,而對無狀態的bean則應該使用singleton作用域。
下圖演示了Spring的prototype作用域。請注意,通常情況下,DAO不會被配置成prototype,因爲DAO通常不會持有任何會話狀態,因此應該使用singleton作用域。
要在XML中將bean定義成prototype,可以這樣配置:
<!-- using spring-beans-2.0.dtd --> <bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/> <!-- the following is equivalent and preserved for backward compatibility in spring-beans.dtd --> <bean id="accountService" class="com.foo.DefaultAccountService" singleton="false"/>
對於prototype作用域的bean,有一點非常重要,那就是Spring不能對一個prototype bean的整個生命週期負責:容器在初始化、配置、裝飾或者是裝配完一個prototype實例後,將它交給客戶端,隨後就對該prototype實例不聞不問了。不管何種作用域,容器都會調用所有對象的初始化生命週期回調方法。但對prototype而言,任何配置好的析構生命週期回調方法都將不會被調用。清除prototype作用域的對象並釋放任何prototype bean所持有的昂貴資源,都是客戶端代碼的職責。(讓Spring容器釋放被prototype作用域bean佔用資源的一種可行方式是,通過使用bean的後置處理器,該處理器持有要被清除的bean的引用。)
談及prototype作用域的bean時,在某些方面你可以將Spring容器的角色看作是Java new操作的替代者。任何遲於該時間點的生命週期事宜都得交由客戶端來處理。(在Section 3.5.1, “生命週期回調”一節中會進一步講述Spring容器中的bean生命週期。)
當使用依賴於prototype bean的singleton-scoped bean時,請注意依賴是在實例化時處理的。這也就是說,如果要把一個prototype-scoped bean注入到singleton-scoped bean,實際上只是實例化一個新的prototype bean注入到 singleton bean...但這是全部。這種情況下,singleton-scoped bean獲得的prototype實例是唯一的。
然而,你可能需要在運行期讓singleton-scoped bean每次都獲得prototype-scoped bean的新實例。在這種情況下,只將prototype-scoped bean注入到你的singleton bean中是沒有用的,因爲正如上文所說的,僅僅在當Spring容器實例化singleton bean並且處理注入的依賴時,生成唯一實例。如果你需要在運行期一次又一次的生成(prototype) bean的新實例,你可以參考Section 3.3.7, “方法注入”
向後兼容性:在XML中指定生命週期作用域 | |
---|---|
如果你在bean定義文件中引用'spring-beans.dtd' DTD, 要顯式說明bean的生命週期作用域你必須使用"singleton"屬性(記住singleton生命週期作用域是默認的)。 如果引用的是'spring-beans-2.0.dtd' DTD或者是Spring 2.0 XSD schema, 那麼需要使用"scope"屬性(因爲"singleton"屬性被刪除了, 新的DTD和XSD文件使用"scope"屬性) 簡單地說,如果你用"singleton"屬性那麼就必須在那個文件裏 引用'spring-beans.dtd' DTD。 如果你用"scope"屬性那麼必須 在那個文件裏引用'spring-beans-2.0.dtd' DTD 或'spring-beans-2.0.xsd' XSD。 |
其他作用域,即request、session以及global session 僅在基於web的應用中使用(不必關心你所採用的是什麼web應用框架)。
Note | |
---|---|
下面介紹的作用域僅僅在使用基於web的Spring ApplicationContext實現(如XmlWebApplicationContext)時有用。 如果在普通的Spring IoC容器中,比如像XmlBeanFactory或ClassPathXmlApplicationContext, 嘗試使用這些作用域,你將會得到一個IllegalStateException異常(未知的bean作用域)。 |
要使用request、session和 global session作用域的bean(即具有web作用域的bean), 在開始設置bean定義之前,還要做少量的初始配置。請注意,假如你只想要“常規的”作用域,(singleton和prototype),就不需要這一額外的設置。
在目前的情況下,根據你的特定servlet環境,有多種方法來完成這一初始設置...
如果你用Spring Web MVC,即用SpringDispatcherServlet或DispatcherPortlet來處理請求,則不需要做特別的配置:DispatcherServlet 和 DispatcherPortlet已經處理了所有有關的狀態
當使用了Spring's DispatcherServlet以外的Servlet 2.4及以上的Web容器時(如使用JSF或Struts),你需要在Web應用的'web.xml'文件中增加 javax.servlet.ServletRequestListener 定義
<web-app> ... <listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class> </listener> ... </web-app>
如果你用的是早期版本的web容器(Servlet 2.4以前的版本),那麼你要使用一個javax.servlet.Filter的實現。請看下面的web.xml配置片段:
<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>
RequestContextListener和RequestContextFilter兩個類做的都是同樣的工作: 將HTTP request對象綁定到爲該請求提供服務的Thread。 這使得具有request和session作用域的bean能夠在後面的調用鏈中被訪問到。
考慮下面bean定義:
<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>
針對每次HTTP請求,Spring容器會根據loginAction bean定義創建一個全新的LoginAction bean實例, 且該loginAction bean實例僅在當前HTTP request內有效,因此可以根據需要放心的更改所建實例的內部狀態, 而其他請求中根據loginAction bean定義創建的實例,將不會看到這些特定於某個請求的狀態變化。 當處理請求結束,request作用域的bean實例將被銷燬。
考慮下面bean定義:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
針對某個HTTP Session,Spring容器會根據userPreferences bean定義創建一個全新的userPreferences bean實例, 且該userPreferences bean僅在當前HTTP Session內有效。 與request作用域一樣,你可以根據需要放心的更改所創建實例的內部狀態,而別的HTTP Session中根據userPreferences創建的實例, 將不會看到這些特定於某個HTTP Session的狀態變化。 當HTTP Session最終被廢棄的時候,在該HTTP Session作用域內的bean也會被廢棄掉。
考慮下面bean定義:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="globalSession"/>
global session作用域類似於標準的HTTP Session作用域,不過它僅僅在基於portlet的web應用中才有意義。Portlet規範定義了全局Session的概念,它被所有構成某個portlet web應用的各種不同的portlet所共享。在global session作用域中定義的bean被限定於全局portlet Session的生命週期範圍內。
請注意,假如你在編寫一個標準的基於Servlet的web應用,並且定義了一個或多個具有global session作用域的bean,系統會使用標準的HTTP Session作用域,並且不會引起任何錯誤。
能夠在HTTP request或者Session(甚至自定義)作用域中定義bean固然很好,但是Spring IoC容器除了管理對象(bean)的實例化,同時還負責協作者(或者叫依賴)的實例化。如果你打算將一個Http request範圍的bean注入到另一個bean中,那麼需要注入一個AOP代理來替代被注入的作用域bean。也就是說,你需要注入一個代理對象,該對象具有與被代理對象一樣的公共接口,而容器則可以足夠智能的從相關作用域中(比如一個HTTP request)獲取到真實的目標對象,並把方法調用委派給實際的對象。
Note | |
---|---|
<aop:scoped-proxy/> 不能和作用域爲singleton或prototype的bean一起使用。爲singleton bean創建一個scoped proxy將拋出BeanCreationException異常。 |
讓我們看一下將相關作用域bean作爲依賴的配置,配置並不複雜(只有一行),但是對理解“爲何這麼做”以及“如何做”是很重要的。
<?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-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"> <!-- a HTTP Session-scoped bean exposed as a proxy --> <bean id="userPreferences" class="com.foo.UserPreferences" scope="session"> <!-- this next element effects the proxying of 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>
要創建這樣的代理,只需要在Bean作用域定義中增加一個<aop:scoped-proxy/>子元素(爲了讓容器可以有效的使用基於類(而不是接口)的代理,你需要在classpath中加入CGLIB包, 並且要使用Appendix A, XML Schema-based configuration配置方式)。爲什麼在request,session, globalSession 和 '自定義作用域' 需要<aop:scoped-proxy/>元素?在下面配置片段中可以找到解釋(注意下面 'userPreferences' Bean定義是不完整的):
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/> <bean id="userManager" class="com.foo.UserManager"> <property name="userPreferences" ref="userPreferences"/> </bean>
從上述配置中可以很明顯的看到singleton bean userManager被注入了一個指向HTTP Session作用域bean userPreferences的引用。singleton userManager bean會被容器僅實例化一次,並且其依賴(即userPreferences bean)也僅被注入一次。這意味着,userManager在理論上只會操作同一個userPreferences對象,即原先被注入的那個bean。而注入一個HTTP Session作用域的bean作爲依賴,有違我們的初衷。因爲我們想要的只是一個userManager對象,在它進入一個HTTP Session生命週期時,我們希望去使用一個HTTP Session的userPreferences對象。
當注入某種類型對象時,該對象實現了和UserPreferences類一樣的公共接口(即UserPreferences實例)。並且不論我們底層選擇了何種作用域機制(HTTP request、Session等等),容器都會足夠智能的獲取到真正的 UserPreferences對象,因此我們需要將該對象的代理注入到userManager bean中, 而userManager bean並不會意識到它所持有的是一個指向UserPreferences引用的代理。在本例中,當UserManager實例調用了一個使用UserPreferences對象的方法時,實際調用的是代理對象的方法。隨後代理對象會從HTTP Session獲取真正的UserPreferences對象,並將方法調用委派給獲取到的實際的UserPreferences對象。
這就是當把request-, session-, 和 globalSession-scoped beans 注入到協作對象中時,需要以下的正確而完整的配置:
<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>
默認情況下,當一個bean有<aop:scoped-proxy/>標記時,Spring容器將爲它創建一個基於CGLIB的類代理,這意味着你需要 將CGLIB庫添加到應用的classpath中。
注意:CGLIB代理僅僅攔截public方法的調用!對於非public的方法調用,不會對目標對象產生委託。
你可以將<aop:scoped-proxy/>的屬性'proxy-target-class'設置爲'false'來選擇標準JDK推薦的基於接口的代理,這樣就不需要在應用的classpath中增加額外的庫。但是,這就意味着類必須實現至少一個接口。並且所有的協作者必須通過某一個 接口來引用bean。
<!-- 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>
題爲Section 6.6, “代理機制”的章節也可以有助於理解對選擇基於類還是基於接口的代理的細微差別。
在Spring 2.0中,Spring的bean作用域機制是可以擴展的。這意味着,你不僅可以使用Spring提供的預定義bean作用域;還可以定義自己的作用域,甚至重新定義現有的作用域(不提倡這麼做,而且你不能覆蓋內置的singleton和prototype作用域)。
作用域是由org.springframework.beans.factory.config.Scope接口定義的。要將你自己的自定義作用域集成到Spring容器中,需要實現該接口。你可能想參考Spring框架本身提供的Scope實現來了解如何創建自己的實現,Scope Javadoc展示了創建自定義作用域的實現的更多細節
Scope接口提供了四個方法來處理獲取對象,移除對象和必要的時候'銷燬'對象。
第一個方法可以從作用域中獲取對象。例如,Session作用域的實現會返回一個session-scoped bean(如果不存在,則返回一個綁定了Session引用的新實例)。
Object get(String name, ObjectFactory objectFactory)
第二個方法可以從作用域中移除對象。例如,session作用域的實現可以從session中移除並返回session-scoped bean(如果沒有找到相應名稱的對象昂,則可以返回null)。
Object remove(String name)
第三個方法是註冊作用域析構的回調方法,當作用域銷燬或作用域中的某個對象銷燬時候會執行。請參考Javadoc或Spring Scope的實現獲得更多析構回調的信息。
void registerDestructionCallback(String name, Runnable destructionCallback)
The last method deals with obtaining the conversation identifier for the underlying scope. This identifier is different for each scope. For a session for example, this can be the session identifier.
最後一個方法處理作用域的會話標識。對每一個作用域來說標識是不一樣的。例如,對於session,將獲得session標識
String getConversationId()
在你編寫和測試完一個或多個自定義Scope實現後,你需要讓Spring容器裝配你的作用域。把一個新的Scope 註冊到Spring 容器中的核心方法定義在ConfigurableBeanFactory接口中,下面就是這個方法的示例:
void registerScope(String scopeName, Scope scope);
registerScope(..) 方法的第一個參數是一個作用域的唯一名稱,例如,Spring 容器中的'singleton'和'prototype'。registerScope(..) 方法的第二個參數是你要註冊和使用的自定義Scope的實例。
假如你實現了自定義的Scope,並像下面例子一樣進行了註冊:
// note: the ThreadScope class does not ship with the Spring Framework Scope customScope = new ThreadScope(); beanFactory.registerScope("thread", scope);
你可以象下面一樣來創建自定義作用域的規則:
<bean id="..." class="..." scope="thread"/>
有了自定義作用域的實現,你將不僅僅可以使用以上的註冊方式,還可以使用CustomScopeConfigurer類來進行聲明式註冊,
以下是使用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-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"> <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="thread"> <bean class="com.foo.ThreadScope"/> </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>