Spring 5 中文解析核心篇-IoC容器之Bean做用域

當你建立一個bean的定義時候,你能夠建立一個模版(recipe)經過bean定義的類定義去建立一個真實的實例。bean定義是模版(recipe)的概念很重要,由於這意味着,與使用類同樣,你能夠從一個模版(recipe)建立多個對象實例。html

你不只能夠控制要插入到從特定bean定義建立的對象中的各類依賴項和配置值,還能夠控制從特定bean定義建立的對象的做用域。這種方法是很是有用的和靈活的,由於你能夠選擇經過配置建立的對象的做用域,而沒必要在Java類級別上考慮對象的做用域。bean可以定義部署到一個或多個做用域。Spring框架支撐6種做用域,4種僅僅使用web環境。你能夠建立定製的做用域。java

下面的表格描述了支撐的做用域:git

Scope Description
singleton (默認)將每一個Spring IoC容器的單個bean定義範圍限定爲單個對象實例。
prototype 將單個bean定義的做用域限定爲任意數量的對象實例
request 將單個bean定義的範圍限定爲單個HTTP請求的生命週期。也就是,每一個HTTP請擁有一個被建立的bean實例。僅在Spring ApplicationContext Web容器有效
session 將單個bean定義的範圍限制在HTTP Session生命週期。僅在Spring ApplicationContext Web容器有效
application 將單個bean定義的範圍限制在ServletContext生命週期。僅在Spring ApplicationContext Web容器有效
websocket 將單個bean定義限制在WebSocket生命週期。僅在Spring ApplicationContext Web容器有效

Spring3.0後,線程安全做用域是有效的但默認沒有註冊。更多的信息,查看文檔 SimpleThreadScope。更多關於怎樣去註冊和自定義做用域,查看自定義做用域web

1.5.1 單例bean做用域

單例bean僅僅只有一個共享實例被容器管理,而且全部對具備與該bean定義相匹配的IDbean的請求都會致使該特定bean實例被Spring容器返回。換一種方式,當你定義一個bean的定義而且它的做用域是單例的時候,Spring IoC容器建立經過bean定義的對象定義的實例。這個單例存儲在緩存中,而且對命名bean的全部請求和引用返回的是緩存對象。下面圖片展現了單例bean做用域是怎樣工做的:spring

singleton

Spring的單例bean概念與在GoF設計模式書中的單例模式不一樣。GoF單例硬編碼對應的做用域例如:只有一個特定類的對象實例對每個ClassLoader只建立一個對象實例。最好將Spring單例的範圍描述爲每一個容器和每一個bean(備註:GoF設計模式中的單例bean是針對不一樣ClassLoader來講的,而Spring的單例是針對不一樣容器級別的)。這意味着,若是在單個Spring容器對指定類定義一個beanSpring容器經過bean定義的類建立一個實例。在Spring中單例做用域是默認的。在XML中去定義一個bean爲單例,你能夠定義一個bean相似下面例子:編程

<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- 經過scope指定bean做用域 單例:singleton ,原型:prototype-->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
1.5.2 原型做用域

非單例原型bean的做用域部署結果是在每一次請求指定bean的時候都會建立一個bean實例。也就是,bean被注入到其餘bean或在容器經過getBean()方法調用都會建立一個新bean。一般,爲全部的無狀態bean使用原型做用域而且有狀態bean使用單例bean做用域。設計模式

下面的圖說明Spring的單例做用域:api

prototype

數據訪問對象(DAO)一般不被配置做爲一個原型,由於典型的DAO不會維持任何會話狀態。咱們能夠更容易地重用單例圖的核心。緩存

下面例子在XML中定義一個原型bean安全

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

與其餘做用域對比,Spring沒有管理原型bean的完整生命週期。容器將實例化、配置或以其餘方式組裝原型對象,而後將其交給客戶端,無需對該原型實例的進一步記錄。所以,儘管初始化生命週期回調函數在全部對象上被回調而無論做用域如何,在原型狀況下,配置銷燬生命週期回調是不被回調。客戶端代碼必須清除原型做用域內的對象並釋放原型Bean佔用的昂貴資源。爲了讓Spring容器釋放原型做用域bean所擁有的資源,請嘗試使用自定義beanpost-processor後置處理器,該後處理器包含對須要清理的bean的引用(能夠經過後置處理器釋放引用資源)。

在某些方面,Spring容器在原型範圍內的bean角色是Java new運算符的替代。全部超過該點的生命週期管理都必須由客戶端處理。(更多關於在Spring容器中的bean生命週期,查看生命週期回調)

1.5.3 單例bean與原型bean的依賴

當你使用依賴於原型bean的單例做用域bean時(單例引用原型bean),須要注意的是這些依賴項在初始化時候被解析。所以,若是你依賴注入一個原型bean到一個單例bean中,一個新原型bean被初始化而且依賴注入到一個單例bean。原型實例是惟一一個被提供給單例做用域bean的實例。(備註:單例引用原型bean時原型bean只會有一個)

然而,假設你但願單例做用域bean在運行時重複獲取原型做用域bean的一個新實例。你不能依賴注入一個原型bean到一個單例bean,由於注入只發生一次,當Spring容器實例化單例bean、解析和注入它的依賴時。若是在運行時不止一次須要原型bean的新實例,查看方法注入

1.5.4 Request, Session, Application, and WebSocket Scopes

requestsessionapplication、和websocket做用域僅僅在你使用SpringApplicationContext實現(例如:XmlWebApplicationContext)時有效。若是你將這些做用域與常規的Spring IoC容器(例如ClassPathXmlApplicationContext)一塊兒使用,則會拋出一個IllegalStateException異常,該錯拋出未知的bean做用域。

  • 初始化Web配置

    爲了支持這些bean的做用域在requestsessionapplication、和websocket級別(web做用域bean)。一些次要的初始化配置在你定義你的bean以前是須要的。(這個初始化安裝對於標準的做用域是不須要的:singletonprototype)。

    如何完成這個初始化安裝依賴於你的特定Servlet環境。

    若是在Spring Web MVC中訪問做用域bean,實際上,在由Spring DispatcherServlet處理的請求中,不須要特殊的設置。DispatcherServlet已經暴露了全部相關狀態。

    若是你使用Servlet 2.5 Web容器,請求處理在SpringDispatcherServlet外(例如:當使用JSFStructs),你須要去註冊org.springframework.web.context.request.RequestContextListenerServletRequestListener。對於Servlet 3.0+,這能夠經過使用WebApplicationInitializer接口以編程方式完成。或者,對於舊的容器,增長下面聲明到你的web應用程序web.xml文件:

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

    或者,若是你的監聽器設置有問題,考慮使用SpringRequestContextFilter。過濾器映射取決於周圍的Web應用程序配置。所以你必須適當的改變它。下面的清單顯示web應用程序filter的部分配置:

    <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所作的事情是同樣的,即將HTTP請求對象綁定到爲該請求提供服務的線程。這使得requestsession範圍的bean在調用鏈的更下方可用。

  • Request做用域

    考慮下面的XML關於bean的定義:

    <!--請求做用域爲request-->
    <bean id="loginAction" class="com.something.LoginAction" scope="request"/>

    Spring容器經過使用LoginAction bean定義爲每一個HTTP的請求建立一個LoginAction新實例bean。也就是說,loginAction bean的做用域在HTTP請求級別。你能夠根據須要更改所建立實例的內部狀態。由於從同一loginAction bean定義建立的其餘實例看不到狀態的這些變化。當這個請求處理完成,bean的做用域從request丟棄。(備註:scope="request" 每一個請求是線程級別隔離的、互不干擾)

    當使用註解驅動組件或Java Config時,@RequestScope註解可以賦值一個組件到request做用域。下面的例子展現怎樣使用:

    @RequestScope//指定做用域訪問爲request
    @Component
    public class LoginAction {
        // ...
    }
  • Session做用域

    考慮下面爲bean定義的XML配置:

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

    Spring容器經過使用userPreferences的bean定義爲單個HTTP Session的生命週期內的建立一個UserPreferences的新實例。換句話說,userPreferences bean有效地做用在HTTP會話級別。與請求範圍的Bean同樣,您能夠根據須要任意更改所建立實例的內部狀態,由於知道其餘也在使用從同一`userPreferences Bean定義建立的實例的HTTP Session實例也看不到這些狀態變化,由於它們特定於單個HTTP會話。當HTTP會話最終被丟棄時,做用於該特定HTTP會話的bean也將被丟棄。

    當使用註解驅動組件或Java Config時,@SessionScope註解可以賦值一個組件到session做用域。下面的例子展現怎樣使用:

    @SessionScope
    @Component
    public class UserPreferences {
        // ...
    }
  • Application做用域

    考慮下面的XML關於bean的定義:

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

    Spring容器經過使用appPreferences的bean定義爲整個Web應用建立一個AppPreferences的bean新實例。也就是說,appPreferences的做用域在ServletContext級別而且做爲一個常規的ServletContext屬性被儲存。這個和Spring的單例bean相似,但有兩個重要的區別:每一個ServletContext是一個單例,而不是每一個SpringApplicationContext(在給定的Web應用程序中可能有多個),而且它其實是暴露的,所以做爲ServletContext屬性可見。

    當使用註解驅動組件或Java Config時,@ApplicationScope註解可以賦值一個組件到application做用域。下面的例子展現怎樣使用:

    @ApplicationScope
    @Component
    public class AppPreferences {
        // ...
    }
  • 做用域bean做爲依賴項

    Spring IoC容器不只管理對象(bean)的實例化,並且還管理協同者(或依賴項)的鏈接。(例如)若是要將HTTP請求範圍的Bean注入另外一個做用域更長的Bean,則能夠選擇注入AOP代理來代替已定義範圍的Bean。也就是說,你須要注入一個代理對象,該對象暴露與範圍對象相同的公共接口,但也能夠從相關範圍(例如HTTP請求)中檢索實際目標對象,並將方法調用委託給實際對象。

    在這些 bean做用域是單例之間,你可使用 <aop:scoped-proxy/>。而後經過一個可序列化的中間代理引用,從而可以在反序列化時從新得到目標單例 bean

    當申明 <aop:scoped-proxy/>原型做用域bean,每一個方法調用共享代理致使一個新目標實例被建立,而後將該調用轉發到該目標實例。

    一樣,做用域代理不是以生命週期安全的方式從較短的做用域訪問bean的惟一方法。你也能夠聲明你的注入點(也就是,構造函數或者Setter參數或自動注入字段)例如:ObjectFactory<MyTargetBean>,容許getObject()調用在每次須要時按需檢索當前實例-不保留實例或單獨存儲實例。

    做爲一個擴展的變體,你能夠聲明ObjectProvider<MyTargetBean>,提供了一些附加的獲取方式,包括getIfAvailablegetIfUnique

    JSR-330的這種變體稱爲Provider,並與Provider <MyTargetBean>聲明和每次檢索嘗試的相應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
            https://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/aop
            https://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <!-- 一個HTTP Session做用域的bean暴露爲一個代理 -->
        <bean id="userPreferences" class="com.something.UserPreferences" scope="session">
            <!-- 指示容器代理周圍的bean -->
            <aop:scoped-proxy/> //1.
        </bean>
    
        <!-- 一個單例做用域bean 被注入一個上面的代理bean -->
        <bean id="userService" class="com.something.SimpleUserService">
            <!-- a reference to the proxied userPreferences bean -->
            <property name="userPreferences" ref="userPreferences"/>
        </bean>
    </beans>
    1. 這行定義代理。

建立一個代理,經過插入一個子<aop:scoped-proxy/>元素到一個做用域bean定義中(查看選擇代理類型去建立基於Schema的XML配置)。爲何這些bean的定義在requestsession和自定義做用域須要<aop:scoped-proxy/>元素?考慮如下單例bean定義,並將其與須要爲上述範圍定義的內容進行對比(請注意,如下userPreferences bean定義不完整):

<!--沒有<aop:scoped-proxy/> 元素-->
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
  
<bean id="userManager" class="com.something.UserManager">
      <property name="userPreferences" ref="userPreferences"/>
  </bean>

在前面的例子中,單例bean (userManager) 被注入一個引用到HTTP Session做用域的bean (userPreferences)。這個顯著點是userManager bean是一個單例bean:這個實例在每一個容器值初始化一次,而且它的依賴(在這個例子僅僅一個,userPreferences bean)僅僅被注入一次。這意味着userManager bean運行僅僅在相同的userPreferences對象上(也就是,最初注入的那個)。

當注入一個短生命週期做用域的bean到一個長生命週期做用域bean的時候這個不是咱們指望的方式(例如:注入一個HTTP Session做用域的協同者bean做爲一個依賴注入到單例bean)。相反,你只須要一個userManager對象,而且在HTTP會話的生存期內,你須要一個特定於HTTP會話的userPreferences對象。所以,容器建立一個對象,該對象公開與UserPreferences類徹底相同的公共接口(理想地,對象是UserPreferences實例),能夠從做用域機制(HTTP 請求,Session,以此類推)獲取真正的UserPreferences對象。容器注入這個代理對象到userManager bean,這並不知道此UserPreferences引用是代理。在這個例子中,當UserManager實例調用在依賴注入UserPreferences對象上的方法時,它其實是在代理上調用方法。而後代理從(在本例中)HTTP會話中獲取實際的UserPreferences對象,並將方法調用委託給檢索到的實際UserPreferences對象。

所以,在將request-scopedsession-scoped的bean注入到協做對象中時,你須要如下(正確和完整)配置,如如下示例所示:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
    <aop:scoped-proxy/>
  </bean>
<bean id="userManager" class="com.something.UserManager">
      <property name="userPreferences" ref="userPreferences"/>
  </bean>
  • 代理類型選擇

    默認狀況下,當Spring容器爲bean建立一個代理,這個bean經過<aop:scoped-proxy/>元素被標記,基於CGLIB的類代理被建立。

    CGLIB代理攔截器僅僅公共方法被調用!在代理上不要調用非公共方法。

    或者,你能夠爲做用域bean配置Spring容器建立標準的JDK基於接口的代理,經過指定<aop:scoped-proxy/>元素的proxy-target-class屬性值爲false。使用基於JDK接口的代理意味着你不須要應用程序類路徑中的其餘庫便可影響此類代理(備註:意思是沒有額外的依賴)。可是,這也意味着做用域Bean的類必須實現至少一個接口,而且做用域Bean注入到其中的全部協同者必須經過其接口之一引用該Bean。如下示例顯示基於接口的代理:

    <!-- DefaultUserPreferences implements the UserPreferences interface -->
    <bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
      <!--基於接口代理-->
        <aop:scoped-proxy proxy-target-class="false"/>
    </bean>
    
    <bean id="userManager" class="com.stuff.UserManager">
        <property name="userPreferences" ref="userPreferences"/>
    </bean>

    更多詳細信息關於選擇基於class或基於接口代理,參考代理機制

    參考代碼: com.liyong.ioccontainer.starter.XmlBeanScopeIocContainer
1.5.5 自定義做用域

bean做用域機制是可擴展的。你能夠定義你本身的做用域或者甚至重定義存在的做用域,儘管後者被認爲是很差的作法,你不能覆蓋內置的單例和原型範圍。

  • 建立一個自定義做用域

    去集成你的自定義做用域到Spring容器中,你須要去實現org.springframework.beans.factory.config.Scope接口,在這章中描述。有關如何實現本身的做用域的想法,查看Scope實現提供關於Spring框架自身和Scope的文檔,其中詳細說明了你須要實現的方法。

    Scope接口有四個方法從做用域獲取對象,從做用域移除它們,而且讓它們銷燬。

    例如:Sessonscope實現返回Season做用域bean(若是它不存在,這個方法返回一個新的bean實例,將其綁定到會話以供未來引用)。 下面的方法從底層做用域返回對象:

    Object get(String name, ObjectFactory<?> objectFactory)

    例如:Sessionscope實現移除Season做用域bean從底層的Session。對象應該被返回,可是若是這個對象指定的名稱不存在你也能夠返回null。下面的方法從底層做用域移除對象:

    Object remove(String name)

    如下方法註冊在銷燬做用域或銷燬做用域中的指定對象時應執行的回調:

    void registerDestructionCallback(String name, Runnable destructionCallback)

    查看javadoc或者Spring做用域實現關於更多銷燬回調的信息。

    如下方法獲取基礎做用域的會話標識符:

    String getConversationId()

    這個表示每一個做用域是不一樣的。對於Session做用域的實現,此標識符能夠是Session標識符。

  • 使用自定義做用域

    在你寫而且測試一個或更多自定義Scope實現,你須要去讓Spring容器知道你的新做用域。如下方法是在Spring容器中註冊新範圍的主要方法:

    void registerScope(String scopeName, Scope scope);

    這個方法在ConfigurableBeanFactory接口上被定義,該接口可經過Spring附帶的大多數具體ApplicationContext實現上的BeanFactory屬性得到。

    registerScope(..) 方法第一個參數是惟一的名字關於做用域。Spring容器自己中的此類名稱示例包括單例和原型。registerScope(..) 方法第二個參數是自定義Scope實現

    假設你寫你的自定義Scope實現而且像下面的例子註冊它。

    接下來例子使用 SimpleThreadScope,它包括 Spring可是默認是不被註冊的。對於你本身的自定義範圍實現,是相同的。
    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
            https://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/aop
            https://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="thing2" class="x.y.Thing2" scope="thread">
            <property name="name" value="Rick"/>
            <aop:scoped-proxy/>
        </bean>
    
        <bean id="thing1" class="x.y.Thing1">
            <property name="thing2" ref="thing2"/>
        </bean>
    
    </beans>
    當在 FactoryBean實現中配置 <aop:scoped-proxy/>時,限定做用域的是工廠 bean自己,而不是從 getObject()返回對象。

    參考代碼:com.liyong.ioccontainer.starter.XmlCustomScopeIocContainer

做者

我的從事金融行業,就任過易極付、思建科技、某網約車平臺等重慶一流技術團隊,目前就任於某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大數據、數據存儲、自動化集成和部署、分佈式微服務、響應式編程、人工智能等領域。同時也熱衷於技術分享創立公衆號和博客站點對知識體系進行分享。

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1...

微信公衆號:

技術交流羣:

相關文章
相關標籤/搜索