Spring 核心技術(6)

接上篇:Spring 核心技術(5)

version 5.1.8.RELEASE

1.5 Bean 作用域

創建 bean 定義時,你創建了一種用於創建 bean 定義中定義的類實例的方法。bean定義的設想是一個很重要的方法,因爲它意味着,與一個類一樣,你可以從以一種方式創建許多對象實例。

你不僅可以控制要插入到以特定 bean 定義創建的對象中的各種依賴項和配置值,還可以控制以特定bean定義創建的對象的作用域。這種方法功能強大且靈活,因爲你可以選擇通過配置創建的對象的作用域,而不必在 Java 類級別設定對象的作用域。Bean 可以被部署到定義的多個作用域之一中。Spring Framework 支持六個作用域,其中四個範圍僅在使用支持 Web 的 ApplicationContext 時可用。你還可以創建自定義範圍

下表描述了支持的作用域:

作用域 描述
單例 (默認)將單個 bean 定義的作用域限定爲每個 Spring IoC 容器的單個對象實例。
原型 將單個 bean 定義作用域限定爲任意數量的對象實例。
請求 將單個 bean 定義作用域限定爲單個 HTTP 請求的生命週期。也就是說,每個 HTTP 請求都有自己的 bean 實例,它是在單例 bean 定義的後面創建的。僅在支持 web 的 Spring ApplicationContext 中可用。
會話 將單個 bean 定義作用域限定爲 HTTP 的生命週期 Session。僅在支持 web 的 Spring ApplicationContext 中可用。
應用 將單個 bean 定義作用域限定爲 ServletContext 的生命週期。僅在支持 web 的 Spring ApplicationContext 中可用。
websocket 將單個 bean 定義作用域限定爲 WebSocket 的生命週期。僅在支持 web 的 Spring ApplicationContext 中可用。

從 Spring 3.0 開始,線程作用域可用,但默認情況下不會註冊:請參閱 SimpleThreadScope。從 Spring 4.2 開始,事務作用域可用:請參閱 SimpleTransactionScope。有關如何註冊這些或任何其他自定義作用域的說明,請參閱使用自定義作用域

1.5.1 單例作用域

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

換句話說,當你定義了一個 bean 定義並將其作用域設置爲單例時,Spring IoC 容器只會通過該 bean 定義創建一個​​實例。此單個實例存儲在此類單例 bean 的緩存中,並且該命名 Bean 的所有後續請求和引用都將返回此緩存對象。下圖顯示了單例作用域的工作原理:

Spring 的單例 bean 概念不同於 Gang of Four(GoF)設計模式書中定義的單例模式。GoF 單例對一個對象的作用域進行硬編碼,使得每個 ClassLoader 只能創建一個特定類的實例。Spring 單例的作用域最好描述爲一個容器一個 bean。這意味着,如果在 Spring 容器中定義一個 bean 爲單例,則 Spring 容器只會根據該 bean 定義創建一個實例。單例作用域是 Spring 的默認作用域。要在 XML 中將 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 原型作用域

bean 部署的非單例原型作用域導致每個對該特定 bean 的請求都會創建新的 bean 實例。也就是說,bean 被注入另一個 bean,或者通過調用容器的 getBean() 方法來請求它。通常,你應該對所有有狀態的 bean 使用原型作用域,對無狀態的 bean 使用單例作用域。

下圖說明了 Spring 原型作用域:

(數據訪問對象(DAO)通常不配置爲原型,因爲常用的 DAO 不會保持任何會話狀態。我們更容易重用單例的核心特性。)

以下示例在 XML 中將 bean 定義爲原型作用域:

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

與其他範圍相比,Spring 不管理原型 bean 的完整生命週期。容器實例化、配置和組裝原型對象並將其交給客戶端,但沒有該原型實例的其他記錄。因此,不管對象是什麼作用域,都會調用初始化生命週期的回調方法,但在原型作用域下,不會調用已配置的銷燬生命週期回調。客戶端代碼必須清理原型作用域對象並釋放原型 bean 佔用的昂貴資源。要使 Spring 容器釋放原型作用域的 bean 所擁有的資源,請嘗試使用自定義 bean 後置處理器,它包含對需要清理的 bean 的引用。

在某些方面,Spring 容器中原型 bean 的角色就是 Java new 運算符的替代品。超過該部分的所有生命週期管理必須由客戶端處理。(有關 Spring 容器中 bean 的生命週期的詳細信息,請參閱生命週期回調。)

1.5.3 具有原型 bean 依賴關係的單例 Bean

當使用依賴於原型 bean 的單例作用域 bean 時,請注意依賴項是在實例化時解析。因此,如果通過依賴注入將原型作用域的 bean 注入到單例作用域的 bean 中,則會實例化一個新的原型 bean,然後將依賴注入到單例 bean 中。原型實例是唯一可以提供給單例作用域 bean 的實例。

但是,假如你希望單例作用域的 bean 在運行時重複獲取原型作用域的 bean 的新實例。那麼不能將原型作用域的 bean 依賴注入到單例 bean 中,因爲該注入過程只在 Spring 容器實例化單例 bean 並解析注入其依賴項時發生一次。如果需要在運行時多次使用原型 bean 的新實例,請參閱方法注入

1.5.4 請求、會話、應用及 WebSocket 作用域

requestsessionapplicationwebsocket 作用域只有當你使用一個支持 web 的 Spring ApplicationContext 實現(例如 XmlWebApplicationContext)時可用。如果將這些作用域與常規的 Spring IoC 容器一起使用,例如 ClassPathXmlApplicationContext,將會因爲未知 bean 作用域導致拋出 IllegalStateException 異常。

初始化 Web 配置

爲了支持 bean 在 requestsessionapplicationwebsocket 級別的作用域(即具有 web 作用域 bean),定義 bean 之前需要做少量的初始配置。(標準作用域 singletonprototype 不需要此初始設置。)

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

如果是在 Spring Web MVC 中訪問帶有作用域的 bean,實際上是在 Spring DispatcherServlet 處理的請求中,無需進行特殊設置。 DispatcherServlet 已經設置了相關的狀態。

如果您使用 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>

或者,如果你的監聽器設置存在問題,請考慮使用 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>

DispatcherServletRequestContextListenerRequestContextFilter 做同樣的事情,即將 HTTP 請求對象與爲該請求提供服務的 Thread 綁定。這使得請求和會話作用域的 bean 可以在調用鏈的下游進一步使用。

請求作用域

請看以下使用 XML 配置的 bean 定義:

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

Spring 容器通過爲每個 HTTP 請求使用 loginAction 定義創建一個 LoginAction 的新實例。也就是說,loginAction 的作用域是 HTTP 請求級別。你可以根據需要更改創建的實例的內部狀態,因爲同樣從 loginAction 定義創建的其他實例看不到在狀態中的更改。它們特別針對個別要求。當請求完成處理時,作用於該請求的 bean 將會被廢棄。

使用註解驅動的組件或 Java 配置時,@RequestScope 註釋可用於將組件分配給 request 作用域。以下示例顯示瞭如何執行此操作:

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

會話作用域

請看以下使用 XML 配置的 bean 定義:

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

Spring 容器通過使用 userPreferences 定義爲單個 HTTP Session 創建一個新的 UserPreferences 實例。換句話說,userPreferences bean 的有效作用域是 HTTP Session 級別。與 request 作用域的 bean 一樣,你可以根據需要更改創建的實例的內部狀態,因爲知道同樣使用 userPreferences bean定義創建的其他 HTTP Session 實例看不到這些狀態上的更改,因爲它們特定於單個HTTP Session。當 Session 最終銷燬時,同時也會銷燬作用於該特定 HTTP Session 的 bean。

使用註解驅動的組件或 Java 配置時,可以使用 @SessionScope 註解將組件指定爲 session 作用域。

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

應用作用域

請看以下使用 XML 配置的 bean 定義:

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

Spring 容器通過使用 appPreferences bean 定義爲整個 Web 應用程序創建一個新的 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 請求)中找到真實目標對象,並將方法調用委託給真實對象。

你還可以在單例作用域的 bean 中使用 <aop:scoped-proxy/> ,然後通過可序列化的中間代理進行引用,從而能夠在反序列化時重新獲取目標單例 bean。

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

此外,作用域代理不是以生命週期安全的方式從較小作用域中訪問 bean 的唯一方法。你還可以將注入點(即構造函數或 setter 參數或 autowired 字段)聲明爲 ObjectFactory<MyTargetBean>, 允許在每次有需要時調用 getObject() 找回當前實例,而無需保留實例或單獨存儲它。

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

JSR-330 變量被稱作 Provider,每次嘗試調用相應的 get() 時可通過 Provider<MyTargetBean> 定義使用。有關 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 定義中(請參閱選擇要創建的代理類型基於XML架構的配置)。爲什麼定義 requestsession 以及自定義作用域的 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>

在前面的示例中,HTTP Session 級作用域的 bean (userPreferences) 被作爲依賴項注入到單例 bean (userManager)。這裏的重點是 userManager 是一個單例bean :它在每個容器中只實例化一次,它的依賴關係(在這種情況下只有一個 userPreferences bean)也只注入一次。這意味着 userManager bean 只在完全相同的 userPreferences 對象(即最初注入它的對象)上運行。

將一個壽命較短的 bean 注入到一個壽命較長的 bean,可能不是你想要的結果(例如,將一個 HTTP Session作用域的協作 bean 作爲依賴注入到單例 bean 中)。相反,你需要一個 userManager 對象,並且,在 HTTP Session 的生命週期中,需要一個特定於 HTTP SessionuserPreferences 對象。因此,容器創建一個暴露着與 UserPreferences 類完全相同的公共接口的對象(理想情況下是一個 UserPreferences 實例的對象),該對象可以從作用域機制(HTTP 請求,Session等)中獲取 UserPreferences 對象。容器將此代理對象注入到 userManager bean中,該 bean 並不知道此 UserPreferences 引用的是代理。在這個例子中,當一個 UserManager 實例調用依賴注入的 UserPreferences 對象上的一個方法,它實際上是在代理上調用一個方法。然後,代理會從 HTTP Session中獲取實際的 UserPreferences 對象(在此例子中),並將方法調用委派給獲取到的真實對象。

因此,在將 request 和session` 作用域的 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 代理只攔截公共方法調用!不要在這樣的代理上調用非公共方法。它們不會委託給實際的作用域目標對象。

另外,你可以通過指定 <aop:scoped-proxy/> 元素的 proxy-target-class 屬性的值爲 false 來配置 Spring 容器爲此類作用域 bean 創建基於標準 JDK 接口的代理。使用基於 JDK 接口的代理意味着你不需要在應用程序類路徑中使用其他庫來影響此類代理。但是,這也意味着作用域 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>

有關選擇基於類或基於接口的代理的更多詳細信息,請參閱代理機制

1.5.5 自定義範圍

bean 範圍機制是可擴展的。你可以定義自己的範圍,甚至可以重新定義現有範圍,後者並不是很好的做法,而且你無法覆蓋內置 singleton 和 prototype 範圍。

創建自定義範圍

要將自定義作用域集成到 Spring 容器中,需要實現本節中描述的 org.springframework.beans.factory.config.Scope 接口。有關如何實現自己的作用域的主意,請參閱 Spring Framework 本身和 javadoc 提供的 Scope 實現 ,它們解釋了你需要實現的方法的細節。

Scope 接口有四種方法,可以從作用域中獲取對象,將其從作用域中刪除,然後將其銷燬。

例如,會話作用域實現類返回會話範圍的 bean(如果它不存在,則該方法在將其綁定到會話以供後續引用之後返回該 bean 的新實例)。以下方法從基礎範圍返回對象:

Object get(String name, ObjectFactory objectFactory)

例如,會話作用域實現類實現從基礎會話中刪除會話作用域的 bean。應返回該對象,但如果找不到具有指定名稱的對象,則可以返回 null。以下方法從基礎範圍中刪除對象:

Object remove(String name)

以下方法記錄作用域在銷燬或作用域中指定對象被銷燬時應執行的回調:

void registerDestructionCallback(String name, Runnable destructionCallback)

有關銷燬回調的更多信息,請參閱 javadoc 或 Spring 作用域實現。

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

String getConversationId()

每個作用域的標識符都不同。對於會話作用域的實現,該標識符可以是會話的標識符。

使用自定義範圍

在編寫並測試一個或多個自定義 Scope 實現之後,你需要讓 Spring 容器知道你的新作用域。以下方法是向 Spring 容器註冊新 Scope 的核心方法:

void registerScope(String scopeName, Scope scope);

此方法在 ConfigurableBeanFactory 接口上聲明,該接口可通過 Spring 大多數 ApplicationContext 的具體實現的 BeanFactory 屬性獲得。

registerScope(..) 方法的第一個參數是與作用域關聯的唯一名稱。Spring 容器本身中的這些名稱的示例是 singletonprototyperegisterScope(..) 方法的第二個參數是你希望註冊和使用的自定義實現的 Scope 實際實例。

假設你編寫了自定義 Scope 實現,然後註冊它,如下一個示例所示。

下面的示例使用 SimpleThreadScope,它包含在 Spring 中,但默認情況下未註冊。你自己的自定義 Scope 實現使用的指令是相同的。

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

然後,您可以創建符合自定義的作用域規則的 bean 定義, 如下所示:

<bean id="..." class="..." scope="thread">

使用自定義 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() 返回的對象。

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