1. 程式人生 > >Bean的四種作用域

Bean的四種作用域

Bean的作用域

當開發者定義Bean的時候,同時也會定義了該如何建立Bean例項。這些具體建立的過程是很重要的,因為只有通過對這些過程的配置,開發者才能建立例項物件。

開發者不僅可以控制注入不同的依賴到Bean之中,也可以配置Bean的作用域。這種方法是非常強大而且彈性也非常好的。開發者可以通過配置來指定物件的作用域,而不用在Java類層次上來配置。Bean可以配置多種作用域。 

Spring框架支援5種作用域,有三種作用域是當開發者使用基於web的ApplicationContext的時候才生效的。

下面就是Spring直接支援的作用域了,當然開發者也可以自己定製作用域。

作用域

描述

單例(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的文件。想要了解如何註冊這個或者其他的自定義的作用域,可以參考後面的內容。

單例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是不會有任何的狀態的。

下面的例子展示了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仍然屬於只有一個例項的。

請求,會話,全域性會話的作用域

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來處理的,也就沒有其他特殊的初始化配置。DispatcherServlet和DispatcherPortlet已經包含了相關的狀態。

如果使用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>

DispatcherServlet,RequestContextListener以及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"/>


  1.  

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

  1. application作用域是每個ServletContext中包含一個,而不是每個SpringApplicationContext之中包含一個(某些應用中可能包含不止一個ApplicationContext)。
  2. 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來獲取指定與某個HTTPSession的userPreferences物件都是不可能的。所以容器建立了一個獲取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容器之中的singleton和prototype就是這樣的名字。第二個引數就是我們根據Scope介面所實現的具體的物件。

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

下面的例子使用了SimpleThreadScope,這個例子Spring中是有實現的,但是沒有預設註冊。開發者自實現的Scope也可以通過如下方式來註冊。

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

  1. 1
  2. 2

之後,開發者可以通過如下類似的Bean定義來使用自定義的Scope:

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

  1. 1

在定製的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>

**歡迎關注我的個人公眾號:we-aibook,裡面有相關技術文章分享,專案架構,知識星球,技術交流群,不定期進行抽獎送書活動喲!**