Spring Framework Core(1)-The Ioc Container(5) Bean 作用域

1 IoC 容器

1.5 Bean 作用域

當您創建一個bean定義時,您將創建一個用於創建由該bean定義定義的類的實際實例的方法。bean定義是模板的想法很重要,因爲它意味着,與類一樣,您可以從一個模板創建多個對象實例。

您不僅可以控制要插入到由特定bean定義創建的對象中的各種依賴項和配置值,還可以控制由特定bean定義創建的對象的範圍。這種方法強大而靈活,因爲您可以選擇通過配置創建的對象的範圍,而不必在Java類級別上考慮對象的範圍。可以將bean定義爲部署在多種作用域中的一種。Spring框架支持6種作用域,其中4種只有在使用web感知的ApplicationContext時纔可用。您還可以創建a custom scope。
下表描述了支持的範圍:

Scope Description
singleton (默認)爲每個Spring IoC容器將單個bean定義作用於單個對象實例
prototype 將單個bean定義作用於任意數量的對象實例。
request 將單個bean定義的範圍限定爲單個HTTP請求的生命週期。也就是說,每個HTTP請求都有自己的bean實例,這些實例是在單個bean定義的基礎上創建的。僅在可感知web的Spring應用程序上下文中有效。
session 將單個bean定義的範圍限定爲HTTP會話的生命週期。僅在可感知web的Spring應用程序上下文中有效。
application 將單個bean定義作用於ServletContext的生命週期。僅在可感知web的Spring應用程序上下文中有效。
websocket 將單個bean定義作用於WebSocket的生命週期。僅在可感知web的Spring應用程序上下文中有效。

從Spring 3.0開始,線程作用域可用,但默認情況下不註冊。有關更多信息,請參閱SimpleThreadScope的文檔。有關如何註冊此或任何其他自定義範圍的說明,請參閱Using a Custom Scope。

1.5.1 The Singleton Scope

只管理一個單例bean的一個共享實例,所有對具有與該bean定義匹配的ID或ID的bean的請求都會導致Spring容器返回該特定bean實例。

換句話說,當您定義一個bean定義並將其定義爲一個單例對象時,Spring IoC容器只創建該bean定義定義的對象的一個實例。此單一實例存儲在此類單例bean的緩存中,該指定bean的所有後續請求和引用都將返回緩存的對象。下圖顯示了單例範圍的工作方式:
在這裏插入圖片描述Spring的單例bean概念與《設計模式:可複用面向對象軟件的基礎》(四人組(GoF)模式)書中定義的單例模式不同。例對象對對象的作用域進行硬編碼,這樣每個類裝入器只能創建一個特定類的實例。Spring單例的範圍最好描述爲每個容器和每個bean。這意味着,如果您在單個Spring容器中爲特定類定義一個bean,那麼Spring容器將創建由該bean定義定義的類的一個且僅一個實例。單例範圍是Spring的默認範圍。要在XML中將bean定義爲單例,您可以定義如下例所示的bean:

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

<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

1.5.2 The Prototype Scope

bean部署的非單例原型範圍導致在每次發出對特定bean的請求時創建一個新的bean實例。也就是說,bean被注入到另一個bean中,或者您通過容器上的getBean()方法調用請求它。通常,您應該爲所有有狀態bean使用 Prototype Scope,爲無狀態bean使用單例範圍
下圖說明了Spring prototype作用域
在這裏插入圖片描述
數據訪問對象(DAO)通常不配置爲原型,因爲典型的DAO不包含任何會話狀態。我們可以更容易地重用單例圖的核心:
下面的示例將bean定義爲XML中的prototype

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

與其他作用域不同,Spring不管理prototype bean的完整生命週期。容器實例化、配置或以其他方式組裝prototype對象並將其交給客戶機,而不需要該prototype實例的進一步記錄。因此,儘管初始化生命週期回調方法在所有對象上都被調用,而與範圍無關,但是在prototypes的情況下,配置的銷燬生命週期回調不會被調用。客戶端代碼必須清理prototype-scoped的對象,並釋放 prototype bean持有的寶貴資源。嘗試使用自定義bean post-processor,它包含需要清理的bean的引用。

在某些方面,Spring容器在 prototype-scoped bean方面的角色可以替代Java new操作符,所有超過那個點的生命週期管理都必須由客戶端處理。(有關Spring容器中bean生命週期的詳細信息,請參閱Lifecycle Callbacks。)

1.5.3 具有Prototype-bean依賴項的Singleton Beans

當您使用依賴於prototype bean的singleton-scoped bean時,請注意依賴項是在實例化時解析的。
因此,如果您依賴地將一個prototype-scoped bean注入到一個singleton-scoped bean中,一個新的prototype bean將被實例化,然後依賴地注入到singleton bean中,prototype實例是惟一提供給單例作用域bean的實例。

但是,假設您希望singleton-scoped bean在運行時重複獲取prototype-scoped bean的新實例。您不能依賴地將一個prototype-scoped bean注入到您的singleton bean中,因爲當Spring容器實例化singleton bean並解析和注入它的依賴項時,這種注入只發生一次。如果在運行時需要原型bean的新實例超過一次,請參考 Method Injection

1.5.4 Request, Session, Application, and WebSocket 作用域

只有在使用web感知的Spring ApplicationContext實現(如XmlWebApplicationContext)時,Request, Session, Application 和 WebSocket 作用域纔可用。如果您將這些作用域與常規的Spring IoC容器(如ClassPathXmlApplicationContext)一起使用,則會拋出一個IllegalStateException,它會報錯一個未知的bean作用域。

初始化web配置

爲了在請求、會話、應用程序和websocket級別(web範圍的bean)上支持bean的作用域,需要在定義bean之前進行少許初始配置。(標準作用域單例和原型不需要這個初始設置。)

如何完成這個初始設置取決於特定的Servlet環境。

如果您在Spring Web MVC中訪問作用域bean,實際上,在由Spring DispatcherServlet處理的請求中,不需要特殊的設置。DispatcherServlet已經公開了所有相關狀態。

如果您使用Servlet 2.5 web容器,並在Spring的DispatcherServlet之外處理請求(例如,在使用JSF或Struts時),您需要註冊org.springframe .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應用程序的過濾部分:

<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請求對象綁定到服務該請求的線程。這使得在請求和會話範圍內的bean可以在調用鏈的更底層使用。

Request 作用域

考慮以下bean定義的XML配置:

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

在這裏插入圖片描述

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

Session 作用域

考慮以下bean定義的XML配置:

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

Spring容器通過爲單個HTTP會話的生命週期使用UserPreferences bean定義來創建UserPreferences bean的新實例。換句話說,userPreferences bean有效地限定在HTTP會話級別。與使用請求作用域bean一樣,您可以根據需要更改所創建實例的內部狀態,因爲您知道其他使用相同userPreferences bean定義創建的實例的HTTP會話實例不會在狀態中看到這些更改,因爲他們各自在獨立的HTTP會話。當HTTP會話最終被丟棄時,作用域爲該特定HTTP會話的bean也被丟棄。
在使用註解驅動的組件或Java配置時,可以使用@SessionScope註解將組件分配給Session 作用域。

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

Application 作用域

考慮以下bean定義的XML配置:

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

通過爲整個web應用程序使用一次AppPreferences bean定義,Spring容器創建了AppPreferences bean的一個新實例。也就是說,appPreferences bean的作用域在ServletContext級別,並存儲爲一個常規的ServletContext屬性。
這有點類似於Spring單例bean,但在兩個重要方面有所不同:它是每個ServletContext的單例對象,而不是每個Spring 'ApplicationContext’的單例對象(在任何給定的web應用程序中可能有多個); 它實際上是公開的,因此作爲ServletContext屬性可見。
在使用註解驅動的組件或Java配置時,可以使用@ApplicationScope註解將組件分配給Application作用域:

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

設置了作用域的Bean作爲依賴

Spring IoC容器不僅管理對象(bean)的實例化,還管理協作者(或依賴項)的連接。如果您想將(例如)一個HTTP請求作用域的bean注入到另一個更長的作用域的bean中,您可以選擇注入一個AOP代理來代替作用域的bean。也就是說,您需要注入一個代理對象,它與作用域對象公開相同的公共接口,但也可以從相關作用域(如HTTP請求)檢索實際目標對象,並將方法調用委託給實際對象。

你也可以在定義爲singleton的bean之間使用<aop:scoped-proxy/>,然後引用通過一個可序列化的中間代理,因此能夠在反序列化時重新獲得目標singleton bean。

當對作用域爲prototype的bean聲明<aop:scoped-proxy/>時,共享代理上的每個方法調用都會導致創建一個新的目標實例,然後將調用轉發給該實例。

而且,作用域代理不是以生命週期安全的方式從較短作用域訪問bean的惟一方法。您還可以將注入點(即構造函數或setter參數或自動裝配字段)聲明爲ObjectFactory<MyTargetBean>允許getObject()調用在每次需要時根據需要檢索當前實例——而不需要保持實例或單獨存儲它。

作爲擴展的變體,您可以聲明ObjectProvider<MyTargetBean>,它提供了幾個額外的訪問變體,包括getIfAvailable和getIfUnique.

該方法的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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- an HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.something.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.something.SimpleUserService">
        <!-- a reference to the proxied userPreferences bean -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>

要創建這樣的代理,需要將一個子<aop:scoped-proxy/>元素插入到有作用域的bean定義中. (see Choosing the Type of Proxy to Create and XML Schema-based configuration)。爲什麼在request、session和自定義範圍級別定義作用域的bean需要<aop:scoped-proxy/>元素。考慮下面的單例bean定義,並將其與您需要爲前面提到的作用域定義的內容進行對比(請注意,下面的userPreferences bean定義是不完整的)

<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會話範圍的bean的引用(userPreferences)。這裏的要點是userManager bean是單例的:它僅針對每個容器實例化一次,其依賴項(在本例中只有一個,即userPreferences bean)也僅注入一次。這意味着userManager bean只對完全相同的userPreferences對象(即最初注入它的對象)進行操作。

這不是您在將一個較短的作用域bean注入到一個較長的作用域bean時想要的行爲(例如,將一個HTTP會話作用域的協作bean作爲依賴項注入到單例bean中)。
相反,您需要一個單一的userManager對象,並且對於HTTP會話的生存期,您需要一個特定於HTTP會話的userPreferences對象。因此,容器創建一個對象,該對象公開與UserPreferences類完全相同的公共接口(理想情況下是UserPreferences實例的對象),該對象可以從作用域機制(HTTP請求、會話,等等)獲取真正的UserPreferences對象。容器將此代理對象注入userManager bean,該bean不知道此UserPreferences引用是代理。在本例中,當UserManager實例調用依賴注入的UserPreferences對象上的方法時,它實際上是在調用代理上的方法。然後代理從HTTP會話中獲取真實的UserPreferences對象,並將方法調用委託給檢索到的真實的UserPreferences對象。因此,在將請求和會話範圍的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容器爲用<aop:scoped-proxy/>元素標記的bean創建代理時,將創建一個基於cglib的類代理。

CGLIB代理只攔截公共方法調用!不要在這樣的代理上調用非公共方法。它們沒有被委託給實際作用域的目標對象

或者,您可以配置Spring容器,爲這種作用域bean創建標準的基於JDK接口的代理,方法是爲<aop:scoped-proxy/>元素的代理目標類屬性的值指定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>

有關選擇基於類或基於接口的代理的詳細信息,請看這裏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章