Spring核心技術(五)——Spring中Bean的做用域

前文概述了Spring的容器,Bean,以及依賴的一些信息,本文將描述一下Bean的做用域javascript

Bean的做用域

當開發者定義Bean的時候,同時也會定義了該如何建立Bean實例。這些具體建立的過程是很重要的,由於只有經過對這些過程的配置,開發者才能建立實例對象。html

開發者不只能夠控制注入不一樣的依賴到Bean之中,也能夠配置Bean的做用域。這種方法是很是強大並且彈性也很是好的。開發者能夠經過配置來指定對象的做用域,而不用在Java類層次上來配置。Bean能夠配置多種做用域。
Spring框架支持5種做用域,有三種做用域是當開發者使用基於web的ApplicationContext的時候才生效的。java

下面就是Spring直接支持的做用域了,固然開發者也能夠本身定製做用域。web

做用域 描述
單例(singleton) (默認)每個Spring IoC容器都擁有惟一的一個實例對象
原型(prototype) 一個Bean定義,任意多個對象
請求(request) 一個HTTP請求會產生一個Bean對象,也就是說,每個HTTP請求都有本身的Bean實例。只在基於web的Spring ApplicationContext中可用
會話(session) 限定一個Bean的做用域爲HTTPsession的生命週期。一樣,只有基於web的Spring ApplicationContext才能使用
全局會話(global session) 限定一個Bean的做用域爲全局HTTPSession的生命週期。一般用於門戶網站場景,一樣,只有基於web的Spring ApplicationContext可用
應用(application) 限定一個Bean的做用域爲ServletContext的生命週期。一樣,只有基於web的Spring ApplicationContext可用

在Spring 3.0中,線程做用域是可用的,但不是默認註冊的。想了解更多的信息,能夠參考本文後面關於SimpleThreadScope的文檔。想要了解如何註冊這個或者其餘的自定義的做用域,能夠參考後面的內容。spring

單例Bean

單例Bean全局只有一個共享的實例,全部將單例Bean做爲依賴的狀況下,容器返回將是同一個實例。編程

換言之,當開發者定義一個Bean的做用域爲單例時,Spring IoC容器只會根據Bean定義來建立該Bean的惟一實例。這些惟一的實例會緩存到容器中,後續針對單例Bean的請求和引用,都會從這個緩存中拿到這個惟一的實例。設計模式

Spring的單例Bean和與設計模式之中的所定義的單例模式是有所區別的。設計模式中的單例模式是將一個對象的做用域硬編碼的,一個ClassLoader只有惟一的一個實例。
而Spring的單例做用域,是基於每一個容器,每一個Bean只有一個實例。這意味着,若是開發者根據一個類定義了一個Bean在單個的Spring容器中,那麼Spring容器會根據Bean定義建立一個惟一的Bean實例。
單例做用域是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"/>

原型Bean

非單例的,原型的Bean指的就是每次請求Bean實例的時候,返回的都是新實例的Bean對象。也就是說,每次注入到另外的Bean或者經過調用getBean()來得到的Bean都將是全新的實例。
這是基於線程安全性的考慮,若是使用有狀態的Bean對象用原型做用域,而無狀態的Bean對象用單例做用域。安全

下面的例子說明了Spring的原型做用域。DAO一般不會配置爲原型對象,由於典型的DAO是不會有任何的狀態的。markdown

下面的例子展現了XML中如何定義一個原型的Bean:

<bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/>

與其餘的做用域相比,Spring是不會徹底管理原型Bean的生命週期的:Spring容器只會初始化,配置以及裝載這些Bean,傳遞給Client。可是以後就不會再去管原型Bean以後的動做了。
也就是說,初始化生命週期回調方法在全部做用域的Bean是都會調用的,可是銷燬生命週期回調方法在原型Bean是不會調用的。因此,客戶端代碼必須注意清理原型Bean以及釋放原型Bean所持有的一些資源。
能夠經過使用自定義的bean post-processor來讓Spring釋放掉原型Bean所持有的資源。

在某些方面來講,Spring容器的角色就是取代了Java的new操做符,全部的生命週期的控制須要由客戶端來處理。

單例Bean依賴原型Bean

當使用單例Bean的時候,而該Bean的依賴是原型Bean的時候,須要注意的是依賴的解析都是在初始化的階段的。所以,若是將原型Bean注入到單例的Bean之中,只會請求一次原型的Bean,而後注入到單例的Bean之中。這個依賴的原型Bean仍然屬於只有一個實例的。

然而,假設你須要單例Bean對原型的Bean的依賴須要每次在運行時都請求一個新的實例,那麼你就不可以將一個原型的Bean來注入到一個單例的Bean當中了,由於依賴注入只會進行一次。當Spring容器在實例化單例Bean的時候,就會解析以及注入它所需的依賴。若是實在須要每次都請求一個新的實例,能夠參考Spring核心技術IoC容器(四)中的方法注入部分。

請求,會話,全局會話的做用域

request,session以及global session這三個做用域都是隻有在基於web的SpringApplicationContext實現的(好比XmlWebApplicationContext)中才能使用。
若是開發者僅僅在常規的Spring IoC容器中好比ClassPathXmlApplicationContext中使用這些做用域,那麼將會拋出一個IllegalStateException來講明使用了未知的做用域。

Web初始化配置

爲了可以使用request,session以及global session做用域(web範圍的Bean),須要在配置Bean以前配置作一些基礎的配置。(對於標準的做用域,好比singleton以及prototype,是無需這些基礎的配置的)

具體如何配置取決於Servlet的環境。

好比若是開發者使用了Spring Web MVC框架的話,每個請求會經過Spring的DispatcherServlet或者DispatcherPortlet來處理的,也就沒有其餘特殊的初始化配置。DispatcherServletDispatcherPortlet已經包含了相關的狀態。

若是使用Servlet 2.5的web容器,請求不是經過Spring的DispatcherServlet(好比JSF或者Struts)來處理。那麼開發者須要註冊org.springframework.web.context.request.RequestContextListener或者ServletRequestListener
而在Servlet 3.0之後,這些都可以經過WebApplicationInitializer接口來實現。或者,若是是一些舊版本的容器的話,能夠在web.xml中增長以下的Listener聲明:

<web-app>
    ...
    <listener>
        <listener-class>
            org.springframework.web.context.request.RequestContextListener
        </listener-class>
    </listener>
    ...
</web-app>

若是是對Listener不甚熟悉,也能夠考慮使用Spring的RequestContextFilter。Filter的映射取決於web應用的配置,開發者能夠根據以下例子進行適當的修改。

<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>

DispatcherServletRequestContextListener以及RequestContextFilter作的本質上徹底一致,都是綁定request對象到服務請求的Thread上。這才使得Bean在以後的調用鏈上在請求和會話範圍上可見。

請求做用域

參考以下的Bean定義

<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>

Spring容器會在每次用到loginAction來處理每一個HTTP請求的時候都會建立一個新的LoginAction實例。也就是說,loginActionBean的做用域是HTTPRequest級別的。
開發者能夠隨意改變實例的狀態,由於其餘經過loginAction請求來建立的實例根本看不到開發者改變的實例狀態,全部建立的Bean實例都是根據獨立的請求來的。當請求處理完畢,這個Bean也會銷燬。

會話做用域

參考以下的Bean定義:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>

Spring容器會在每次調用到userPreferences在一個單獨的HTTP會話週期來建立一個新的UserPreferences實例。換言之,userPreferencesBean的做用域是HTTPSession級別的。
request-scoped做用域的Bean上,開發者能夠隨意的更改實例的狀態,一樣,其餘的HTTPSession基本的實例在每一個Session都會請求userPreferences來建立新的實例,因此開發者更改Bean的狀態,對於其餘的Bean仍然是不可見的。當HTTPSession銷燬了,那麼根據這個Session來建立的Bean也就銷燬了。

全局會話做用域

該部分主要是描述portlet的,詳情能夠Google更多關於portlet的相關信息。

參考以下的Bean定義:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="globalSession"/>

global session做用域比較相似以前提到的標準的HTTPSession,這種做用域是隻應用於基於門戶(portlet-based)的web應用的上下之中的。門戶的Spec中定義的global session的意義:global session被全部構成門戶的web應用所共享。定義爲global session做用域的BEan是做用在全局門戶Session的聲明週期的。

若是在使用標準的基於Servlet的Web應用,並且定義了global session做用域的Bean,那麼只是會使用標準的HTTPSession做用域,不會報錯。

應用做用域

考慮以下的Bean定義:

<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>

Spring容器會在整個web應用使用到appPreferences的時候建立一個新的AppPreferences的實例。也就是說,appPreferencesBean是在ServletContext級別的,好似一個普通的ServletContext屬性同樣。這種做用域在一些程度上來講和Spring的單例做用域是極爲類似的,可是也有以下不一樣之處:

  • application做用域是每一個ServletContext中包含一個,而不是每一個SpringApplicationContext之中包含一個(某些應用中可能包含不止一個ApplicationContext)。
  • application做用域僅僅做爲ServletContext的屬性可見,單例Bean是ApplicationContext可見。

做爲依賴

Spring IoC容器不只僅管理對象(Bean)的實例化,同時也負責裝載依賴。若是開發者想裝載一個Bean到一個做用域更廣的Bean當中去(好比HTTP請求返回的Bean),那麼開發者選擇注入一個AOP代理而不是短做用域的Bean。也就是說,開發者須要注入一個代理對象,這個代理對象既能夠找到實際的Bean,也可以建立一個全新的Bean。

開發者會在單例Bean中使用<aop:scoped-proxy/>標籤,來引用一個代理,這個代理的做用就是用來獲取指定的Bean。
當生命使用<aop:scoped-proxy/>來生成一個原型Bean的時候,每一個經過代理的調用都會產生一個新的目標實例。
而且,做用域代理並非惟一來獲取短做用域Bean的惟一安全的方式。開發者也能夠經過簡單的聲明注入爲ObjectFactory<MyTargetBean>,別容許經過蕾西getObject()之類的調用來獲取一些指定的依賴,而不是單獨儲存依賴的實例。
JSR-330關於這部分的不一樣叫作
Provider,經過使用Provider聲明和一個相關的get()方法來獲取指定的依賴。詳細關於JSR-330的信息能夠進去詳細瞭解。

請參考下面的例子:

<?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>

使用代理,只須要在短做用域的Bean定義之中加入一個子節點<aop:scoped-proxy/>便可。Spring核心技術IoC容器(四)中的方法注入中就說起到了Bean依賴的一些問題,這也是咱們爲何要使用aop代理的緣由。假設咱們沒有使用aop代理而是直接進行依賴注入,參考以下的例子:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>

<bean id="userManager" class="com.foo.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

上面的例子中,userManager明顯是一個單例的Bean,注入了一個HTTPSession級別的userPreferences依賴,顯然的問題就是userManager在Spring容器中只會實例化一次,而依賴(當前例子中的userPreferences)也只能注入一次。這也就意味着userManager每次使用的都是相同的userPreferences對象。

那麼這種狀況就絕對不是開發者想要的那種將短做用域注入到長做用域Bean中的狀況了,舉例來講,注入一個HTTPSession級別的Bean到一個單例之中,或者說,當開發者經過userManager來獲取指定與某個HTTPSessionuserPreferences對象都是不可能的。因此容器建立了一個獲取UserPreferences對象的接口,這個接口能夠根據Bean對象做用域機制來獲取與做用域相關的對象(好比說HTTPRequest或者HTTPSession等)。容器以後注入代理對象到userManager中,而意識不到所引用UserPreferences是代理。在這個例子之中,當UserManager實例調用方法來獲取注入的依賴UserPreferences對象時,其實只會調用了代理的方法,由代理去獲取真正的對象,在這個例子中就是HTTPSession級別的Bean。

因此當開發者但願可以正確的使用配置request,session或者globalSession級別的Bean來做爲依賴時,須要進行以下的相似配置:

<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>

選擇代理的類型

默認狀況下,Spring容器建立代理的時候標記爲<aop:scoped-proxy/>的標籤時,會建立一個基於CGLIB的代理

CGLIB代理會攔截public方法調用!因此不要在非public方法上使用代理,這樣將不會獲取到指定的依賴。

或者,開發者能夠經過指<aop:scoped-proxy/>標籤的proxy-target-class屬性的值爲false來配置Spring容器來爲這些短做用域的Bean建立一個標準JDK的基於接口的代理。使用JDK基於接口的代理意味着開發者不須要在應用的路徑引用額外的庫來完成代理。固然,這也意味着短做用域的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>

DefaultUserPreferences實現了UserPreferences並且提供了接口來獲取實際的對象。更多的信息能夠參考AOP代理

定製做用域

Bean的做用域機制是可擴展的,開發者能夠定義本身的一些做用域,甚至從新定義已經存在的做用域,可是這一點Spring團隊是不推薦的,而且開發者不可以重寫singleton以及prototype做用域。

建立定製做用域

爲了可以使Spring能夠管理開發者定義的做用域,開發者須要實現org.springframework.beans.factory.config.Scope接口。想知道如何實現開發者本身定義的做用域,能夠參考Spring框架的一些實現或者是Scope的javadoc,裏面會解釋開發者須要實現的一些細節。

Scope接口中含有4個方法來獲取對象,移除對象,容許銷燬等。

下面的方法返回一個存在的做用域的對象。好比說Session的做用域實現,該函數將返回會話做用域的Bean(若是Bean不存在,該方法會建立一個新的實例)

Object get(String name, ObjectFactory objectFactory)

下面的方法會將對象移出做用域。一樣,以Session爲例,該函數會刪除Session做用域的Bean。刪除的對象會做爲返回值返回,當沒法找到對象的時候能夠返回null

Object remove(String name)

下面的方法會註冊一個回調方法,當須要銷燬或者做用域銷燬的時候調用。詳細能夠參考在javadoc和Spring做用域的實現中找到更多關於銷燬回調方法的信息。

void registerDestructionCallback(String name, Runnable destructionCallback)

下面的方法會獲取做用域的區分標識,區分標識區別於其餘的做用域。

String getConversationId()

使用定製做用域

在實現了開發者的自定義做用域以後,開發者還須要讓Spring容器可以識別發現這個新的做用域。下面的方法就是在Spring容器中用來註冊新的做用域的。

void registerScope(String scopeName, Scope scope);

這個方法是在ConfigurableBeanFactory的接口中聲明的,在大多數的ApplicationContext的實現中都是能夠用的,能夠經過BeanFactory屬性來調用。

registerScope(..)方法的第一個參數是做用域相關聯的惟一的一個名字;舉例來講,好比Spring容器之中的singletonprototype就是這樣的名字。第二個參數就是咱們根據Scope接口所實現的具體的對象。

假定開發者實現了自定義的做用域,而後按照以下步驟來註冊。

下面的例子使用了SimpleThreadScope,這個例子Spring中是有實現的,可是沒有默認註冊。開發者自實現的Scope也能夠經過以下方式來註冊。

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);

以後,開發者能夠經過以下相似的Bean定義來使用自定義的Scope:

<bean id="..." class="..." scope="thread">

在定製的Scope中,開發者也不限於僅僅經過編程方式來註冊本身的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>

至此,本文描述了關於Bean做用域的一些基本信息,在下一篇文章中,將會描述Bean的生命週期等信息。

相關文章
相關標籤/搜索