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 作用域
request
,session
,application
和 websocket
作用域只有當你使用一個支援 web 的 Spring ApplicationContext
實現(例如 XmlWebApplicationContext
)時可用。如果將這些作用域與常規的 Spring IoC 容器一起使用,例如 ClassPathXmlApplicationContext
,將會因為未知 bean 作用域導致丟擲 IllegalStateException
異常。
初始化 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 或 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>
DispatcherServlet
,RequestContextListener
和 RequestContextFilter
做同樣的事情,即將 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>
,它提供了幾個額外的訪問變體,包括getIfAvailable
和getIfUnique
。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架構的配置)。為什麼定義 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>
在前面的示例中,HTTP Session
級作用域的 bean (userPreferences
) 被作為依賴項注入到單例 bean (userManager
)。這裡的重點是 userManager
是一個單例bean :它在每個容器中只例項化一次,它的依賴關係(在這種情況下只有一個 userPreferences
bean)也只注入一次。這意味著 userManager
bean 只在完全相同的 userPreferences
物件(即最初注入它的物件)上執行。
將一個壽命較短的 bean 注入到一個壽命較長的 bean,可能不是你想要的結果(例如,將一個 HTTP Session作用域的協作 bean 作為依賴注入到單例 bean 中)。相反,你需要一個 userManager
物件,並且,在 HTTP Session
的生命週期中,需要一個特定於 HTTP Session
的 userPreferences
物件。因此,容器建立一個暴露著與 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 容器本身中的這些名稱的示例是 singleton
和 prototype
。registerScope(..)
方法的第二個引數是你希望註冊和使用的自定義實現的 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()
返回的物件。
- 我的CSDN:https://blog.csdn.net/liweitao7610
- 我的部落格園:https://www.cnblogs.com/aotian/
- 我的簡書:https://www.jianshu.com/u/6b6e162f1fdc