天天看點

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

定義相比對的

ID

bean

的請求都會導緻該特定

bean

執行個體被

Spring

容器傳回。換一種方式,當你定義一個

bean

的定義并且它的作用域是單例的時候,

Spring IoC

容器建立通過

bean

定義的對象定義的執行個體。這個單例存儲在緩存中,并且對命名

bean

的所有請求和引用傳回的是緩存對象。下面圖檔展示了單例

bean

作用域是怎樣工作的:

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

Spring

的單例

bean

概念與在

GoF

設計模式書中的單例模式不同。

GoF

單例寫死對應的作用域例如:隻有一個特定類的對象執行個體對每一個

ClassLoader

隻建立一個對象執行個體。最好将

Spring

單例的範圍描述為每個容器和每個

bean

(備注:

GoF

設計模式中的單例

bean

是針對不同

ClassLoader

來說的,而

Spring

的單例是針對不同容器級别的)。這意味着,如果在單個

Spring

容器對指定類定義一個

bean

Spring

容器通過

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

的單例作用域:

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

資料通路對象(

DAO

)通常不被配置作為一個原型,因為典型的

DAO

不會維持任何會話狀态。我們可以更容易地重用單例圖的核心。

下面例子在

XML

中定義一個原型

bean

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

與其他作用域對比,

Spring

沒有管理原型

bean

的完整生命周期。容器将執行個體化、配置或以其他方式組裝原型對象,然後将其交給用戶端,無需對該原型執行個體的進一步記錄。是以,盡管初始化生命周期回調函數在所有對象上被回調而不管作用域如何,在原型情況下,配置銷毀生命周期回調是不被回調。用戶端代碼必須清除原型作用域内的對象并釋放原型

Bean

占用的昂貴資源。為了讓

Spring

容器釋放原型作用域

bean

所擁有的資源,請嘗試使用自定義

bean

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

request

session

application

、和

websocket

作用域僅僅在你使用

Spring

ApplicationContext

實作(例如:

XmlWebApplicationContext

)時有效。如果你将這些作用域與正常的

Spring IoC

容器(例如

ClassPathXmlApplicationContext

)一起使用,則會抛出一個

IllegalStateException

異常,該錯抛出未知的

bean

  • 初始化Web配置

為了支援這些bean的作用域在

request

session

application

websocket

級别(web作用域bean)。一些次要的初始化配置在你定義你的bean之前是需要的。(這個初始化安裝對于标準的作用域是不需要的:

singleton

prototype

)。

如何完成這個初始化安裝依賴于你的特定

Servlet

環境。

如果在

Spring Web MVC

中通路作用域

bean

,實際上,在由

Spring

DispatcherServlet

處理的請求中,不需要特殊的設定。

DispatcherServlet

已經暴露了所有相關狀态。

如果你使用

Servlet 2.5

Web

容器,請求處理在

Spring

DispatcherServlet

外(例如:當使用

JSF

Structs

),你需要去注冊

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>           

或者,如果你的監聽器設定有問題,考慮使用

Spring

RequestContextFilter

。過濾器映射取決于周圍的

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>           

DispatcherServlet

RequestContextListener

RequestContextFilter

所做的事情是一樣的,即将

HTTP

請求對象綁定到為該請求提供服務的線程。這使得

request

session

範圍的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作用域

bean

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

Spring

appPreferences

的bean定義為整個

Web

應用建立一個

AppPreferences

的bean新執行個體。也就是說,

appPreferences

的作用域在

ServletContext

級别并且作為一個正常的

ServletContext

屬性被儲存。這個和

Spring

的單例bean類似,但有兩個重要的差別:每個

ServletContext

是一個單例,而不是每個

Spring

ApplicationContext

(在給定的

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>

,提供了一些附加的擷取方式,包括

getIfAvailable

getIfUnique

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

的定義在

request

session

和自定義作用域需要

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

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

接口有四個方法從作用域擷取對象,從作用域移除它們,并且讓它們銷毀。

例如:

Sesson

scope

實作傳回

Season

bean

(如果它不存在,這個方法傳回一個新的

bean

執行個體,将其綁定到會話以供将來引用)。 下面的方法從底層作用域傳回對象:

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

Session

scope

實作移除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

微信公衆号:

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

技術交流群:

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