IoC容器5——bean做用域

bean做用域

當建立bean的定義時,就建立了如何建立類實例的規則。bean定義是一個規則的思想很重要,由於這意味着能夠從一個規則建立許多對象的實例,與類同樣。java

從一個特定的bean定義中建立的bean,不只能夠控制它的各類依賴關係和配置值,還能夠控制對象的做用域。這種方法是強大、靈活的,能夠經過配置選擇要建立對象的做用域,而不須要在Java類的層面。能夠爲Bean指定許多做用域中的一個,而且開箱即用,Spring Framework支持七個做用域,其中有五個只在基於web ApplicationContext中有效。web

下面的做用域開箱即用。也能夠建立用戶自定義的做用域。spring

  • singleton 默認的,每個Spring IoC容器都擁有惟一的一個實例對象。
  • prototype 一個bean定義能夠有任何數量的對象實例。
  • request 一個bean定義的做用域適用於單個HTTP請求的生命週期;也就是說,每一個HTTP請求都有本身的bean定義實例。只有在Web ApplicationContext生效。
  • session 一個bean定義的做用域適用於HTTP Session的生命週期。只有在Web ApplicationContext生效。
  • globalSession 一個bean定義的做用域適用於全局的HTTP Session的生命週期。通常只有使用Porlet Context時纔有效。只有在Web ApplicationContext生效。
  • application 一個bean定義的做用域適用於ServletContext的生命週期。只有在Web ApplicationContext生效。
  • websocket 一個bean定義的做用域適用於WebSocket的生命週期。只有在Web ApplicationContext生效。

從Spring 3.0開始,線程做用域可用,但默認狀況下未註冊。編程

1 Singleton

只管理一個共享的singleton bean 的實例;而且全部使用id的bean請求若是匹配到這個bean定義,Spring容器都返回同一個特定的bean的實例。設計模式

換句話說,當定義一個bean定義並將它的做用域設置爲singleton,Spring IoC容器將僅建立一個由該bean定義指定的對象的實例。這個單獨的實例存儲在singleton bean的緩存中,對該命名bean的全部後續請求和引用都返回緩存的對象。緩存

輸入圖片說明

Spring的singleton bean的概念有別於GoF 設計模式書中的單例模式。GoF單例硬編碼對象的做用域,使得每一個ClassLoader只建立一個特定類的實例。每一個容器一個bean是對Spring singleton做用域的最好描述。若是在單個Spring容器中爲指定的類定義一個bean,則Spring容器將建立由bean定義指定的類的惟一一個實例。Singleton做用域是Spring bean的默認做用域。使用XML定義一個singleton 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"/>

2 Prototype

使用prototype 的bean部署方式會在每次請求一個特定bean的時候都建立一個bean的實例。對bean的每次請求意味着將bean注入到其它bean中或者經過調用容器的getBean()方法請求它。應該對有狀態的bean使用prototype做用域,對無狀態的bean使用singleton做用域。websocket

下圖闡述了Spring prototype 做用域。注意,一個dao通常不配置成prototype,由於一個典型的DAO不保持任何的會話狀態;使用這幅圖僅僅是由於能夠重用singleton圖的核心內容。session

輸入圖片說明

下面的例子在XML中將一個bean定義爲prototype:app

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

相對於其它做用域,Spring無論理prototype bean 的整個生命週期:容器實例化、配置並裝配一個prototype對象,最後將其交給客戶,此後再也不記錄prototype實例。所以,儘管全部做用域的bean的initialization生命週期的回掉函數都會被調用,可是在prototype狀況下,配置的destruction生命週期回掉函數不會被調用。客戶端代碼必須本身清楚prototype對象並釋放它持有的昂貴資源。爲了讓Spring容器釋放prototype bean的資源,可使用自定義的bean post-processor,它會持有須要清理的bean的引用。

在某些方面,Spring 規則認爲prototype bean是Java new 操做法的替代。全部new以後的生命週期都必須由客戶端管理。

3 Singleton bean 擁有 prototype bean 依賴

當你使用有prototype bean依賴的singleton bean時,要注意依賴關係是在實例化時被解析。所以若是你依賴注入一個prototype bean到singleton bean中,一個新的prototype bean 會被實例化而且被依賴注入singleton bean。提供給singleton bean的prototype的實例是惟一的。

可是,假設你想在運行時讓singleton bean每次都獲得一個prototype bean的新實例。你就不能將prototype bean依賴注入到singleton bean中,由於注入僅僅發生一次,是在Spring容器實例化singleton bean並解析和注入依賴關係時。若是想要在運行時獲取prototype bean的新實例不止一次,可使用「方法注入」。

4 Request,session,global session,application 和 WebSocket 做用域

Request,session,global session,application 和 WebSocket 做用域僅在使用基於web的Spring應用上下文(例如XmlWebApplicationContext)中可用。若是在普通的Spring IoC容器(例如ClassPathXmlApplicationContext)中使用這些做用域,會拋出IllegalStateException異常來講明未知的bean做用域。

初始化 web 配置

爲支持Request,session,global session,application 和 WebSocket 做用域,在定義bean以前須要一些基礎的初始化配置。(對於標準的做用域,singleton和prototype,不須要這些初始化配置)。

如何進行初始化設置取決於不一樣的Servlet環境。

若是你使用Spring Web MVC來得到做用域中的bean,實際上就是使用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應用的web.xml中添加如下的聲明:

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

若是配置listener時出現問題,可使用Spring的RequestContextFilter。過濾器的映射取決於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都作了相同的事情,即綁定Http請求對象處處理請求的線程。這使得request和session做用域的bean在跟進一步的調用鏈上可用。

Request 做用域

對於下面的XML格式的一個bean定義:

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

Spring容器使用loginAction bean 定義爲每一個HTTP 請求建立一個LoginAction bean的實例。即loginAction bean 的做用域被設置爲Http request級別。能夠爲所欲爲的改變實例的內部狀態,由於從相同的loginAction bean 定義建立的其它實例對這個狀態的修改不可見;它們特定於單個請求。當請求處理完成,request做用域的bean就失效了。

當使用註解的方式配置,@RequestScope註解用來指定request做用域。

@RequestScope
@Component
public class LoginAction {
    // ...
}

Session 做用域

對於下面的XML格式的一個bean定義:

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

Spring容器使用userPreferences bean定義爲每一個HTTP Session生命週期建立一個UserPreferences bean的實例。換言之,userPreference bean的做用域被設置爲Http session級別。能夠爲所欲爲的改變實例的內部狀態,由於從相同的userPreference bean 定義建立的其它實例對這個狀態的修改不可見;它們特定於每一個會話。當會話結束,session做用域的bean就失效了。

當使用註解的方式配置,@SessionScope註解用來指定session做用域。

@SessionScope
@Component
public class UserPreferences {
    // ...
}

Global session 做用域

對於下面的XML格式的一個bean定義:

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

globalSession做用域與標準的HTTP session做用域類似,而且僅應用於基於portlet的web應用的上下文。portlet文檔定義了global session的概念——在全部組成一個portlet web應用的所用portlet中共享的會話。globalSession bean的做用域與global portlet session的生命週期一致。

若是是在標準的基於Servlet的web應用程序中定義了globalSession做用域,會使用標準的Http session做用域,不會發生錯誤。

Application 做用域

對於下面的XML格式的一個bean定義:

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

Spring容器在整個web應用中使用appPreferences bean定義僅僅建立一次AppPreferences的新實例。即appPreferences bean的做用域是ServletContext級別的,它被保存爲ServletContext屬性。它與Spring singleton bean在某種程度上類似,但有如下兩點重要的不一樣:它對於每一個ServletContext是單例,而不是每一個ApplicationContext(一個web應用中可能有多個ApplicationContext);它被暴露爲一個ServletContext屬性。

當使用註解的方式配置,@ApplicationScope註解用來指定application做用域。

@ApplicationScope
@Component
public class AppPreferences {
    // ...
}

做用域bean的依賴

Spring IoC容器不只管理對象的實例化,還組織它們之間的協做(或者依賴)。若是你想注入一個HTTP request做用域的bean到另一個更長生命週期的bean,也許得選擇注入一個AOP代理來替代做用域bean。也就是說,須要注入一個代理對象來暴露與做用域bean相同的公有接口,能夠在相關的做用域(例如HTTP request)取回真正的目標對象而且在實際的對象上調用代理方法。

能夠在singleton bean之間使用aop:scoped-proxy/,經過可序列化的中間代理的引用,能夠在從新得到目標單例bean時將其反序列化。

當聲明瞭一個指向prototype做用域的aop:scoped-proxy/時,共享代理的每一次方法調用都會建立新的目標bean.

做用域代理並非獲取做用域較小的bean惟一的生命週期安全的途徑。也能夠簡單的聲明注入點(即構造函數/setter方法的參數或者自動裝配的字段)爲ObjectFactory<MyTargetBean>,它容許在每次須要的時候調用getObject()方法獲取如今的實例,而不用持用實例或單獨保存它。

JSR-330將其稱爲Provider,使用Provider<MyTargetBean>聲明和相關的get()方法調用。

下面的例子中僅有一行配置,可是去理解背後的原理很重要:

<?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/來建立一個代理。爲何做用域爲request 、session、globalSession和用戶自定義做用域的bean須要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>

上面的例子中,單例bean userManager有一個Http session做用域的bean userPreferences的引用被注入。突出的問題是userManager bean是一個單例:每一個容器只會實例化一次,而且它的依賴關係(在這裏只有一個userPreferences bean)也只被注入一次。這意味着userManager bean僅僅只會操做一個相同的userPreferences對象,即最開始注入的那個。

這並非將一個短生命週期bean注入到長生命週期bean所須要的行爲,例如將一個HTTP session做用域的bean注入到singleton bean中。而 你須要一個userManager對象的單例、每一個session須要一個userPreference對象。所以容器建立了一個對象用來暴露UserPreferences類的相同公有接口,能夠經過這個對象來獲取做用域(HTTP request,Session等)機制上的實際UserPreferences對象。容器將這個代理對象注入userManager bean,並不知道這是UserPreferences 引用的一個代理。在這個例子中,當UserManager實例調用被依賴注入的UserPreferences對象的方法時,它其實是調用了代理的方法。而後代理獲取HTTP Session(在這個例子中)上的實際的UserPreferences對象,而且代理了實際UserPreferences對象的方法調用。

所以,當注入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>

選擇要建立的代理類型

默認狀況下,當使用aop:scoped-proxy/元素來爲bean建立一個代理,使用的是基於CGLIB的類代理。

CGLIB代理僅僅攔截公有方法調用。在此代理商不要待用非公有方法;這不會委派給實際的做用域目標對象。

做爲替代,能夠配置Spring容器爲做用域bean建立標準JDK的基於接口的代理,將aop:scoped-proxy/標籤的proxy-target-class屬性聲明爲false。使用JDK基於接口的代理不須要嚮應用的classpath添加額外的庫。可是,這也要求做用域bean的類必須至少實現一個接口,而且所用被注入到做用域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>

5 用戶自定義做用域

bean做用域機制是可擴展的;能夠定義本身的做用域,或者重定義現有的做用域,儘管重定義被認爲是一種很差的實踐而且不能覆蓋內建的singleton和prototype做用域。

建立用戶自定義做用域

爲了集成自定義做用域到Spring容器,須要實現org.springframework.beans.factory.config.Scope接口。如何實現自定義的做用域,能夠查看Spring Frameworkd自身提供的Scope實現和Scope javadocs,它更詳細的解釋了須要實現的方法。

Scope接口有四個方法用來從做用域獲取對象、從做用域刪除對象,和容許它們被銷燬。

下面的方法從底層做用域返回對象。例如,會話做用域的實現返回session做用域的bean(若是不存在,方法在綁定bean的新實例到session以後返回它)。

Object get(String name, ObjectFactory objectFactory)

下面的方法從底層做用域移除對象。方法須要返回對象,可是當指定名稱的對象不存在時能夠返回null。

Object remove(String name)

下面的方法註冊當scope被銷燬時或scope中的指定對象被銷燬時,scope須要執行的回掉函數。

void registerDestructionCallback(String name, Runnable destructionCallback)

下面的方法獲取底層做用域的會話標識符。不一樣做用域的標識符不一樣。對於會話做用域的實現,這個標識符能夠是會話的標識符。

String getConversationId()

使用用戶自定義做用域

在測試幾個用戶自定義做用域實現後,須要讓Spring容器瞭解你的新做用域。下面的方法是註冊新做用域的核心方法:

void registerScope(String scopeName, Scope scope);

這個方法在ConfigurableBeanFactory接口中聲明,在大多數的具體ApplicationContext實現上可用。

registerScope(...)方法的第一個參數是與做用域相關的惟一名稱;Spring容器自身的名稱例子是singleton和prototype。第二個參數是一個要註冊和使用的Scope實現的實例。

假設實現一個Scope,能夠用以下方法定義:

下面的例子中使用的SimpleThreadScope包含在Spring中,可是默認未被註冊。這個註冊方法對用戶自定義的Scope實現相同。

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

而後就能夠建立遵循自定義範圍規則的bean定義:

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

當在FactoryBean實現中放置aop:scoped-proxy/元素,它做用域工廠bean自身,而不是它的getObject()方法返回的對象。

相關文章
相關標籤/搜索