大數據

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

當你創建一個bean的定義時候,你可以創建一個模版(recipe)通過bean定義的類定義去創建一個真實的實例。bean定義是模版(recipe)的概念很重要,因為這意味著,與使用類一樣,你可以從一個模版(recipe)創建多個對象實例。

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

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

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。更多關於怎樣去註冊和自定義作用域,查看自定義作用域

1.5.1 單例bean作用域

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

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的單例作用域:

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

微信公眾號:

技術交流群:

Leave a Reply

Your email address will not be published. Required fields are marked *