Bean 作用域
當你建立一個 bean 定義時,你實際上是建立了一個用於建立由該 bean 定義所定義的類的實際例項的“配方”。bean 定義是“配方”這個概念非常重要,因為它意味著,就像一個類一樣,你可以從一個“配方”建立許多物件例項。
你不僅可以控制要插入從特定 bean 定義建立的物件的各種依賴項和配置值,還可以控制從特定 bean 定義建立的物件的範圍。這種方法功能強大且靈活,因為你可以透過配置來選擇建立物件的範圍,而不必在 Java 類級別硬編碼物件的範圍。Bean 可以被定義為部署在多種範圍中的一個。Spring 框架支援六種範圍,其中四種只有在使用 web 感知 ApplicationContext 時才可用。你還可以建立自定義範圍。
下表描述了支援的範圍
| 作用域 | 描述 |
|---|---|
(預設)為每個 Spring IoC 容器將單個 bean 定義限定為單個物件例項。 |
|
將單個 bean 定義限定為任意數量的物件例項。 |
|
將單個 bean 定義限定為單個 HTTP 請求的生命週期。也就是說,每個 HTTP 請求都有其自己的 bean 例項,該例項根據單個 bean 定義建立。僅在 web 感知 Spring |
|
將單個 bean 定義限定為 HTTP |
|
application(應用) |
|
將單個 bean 定義限定為 |
執行緒作用域可用,但預設情況下未註冊。有關更多資訊,請參閱 SimpleThreadScope 的文件。有關如何註冊此作用域或任何其他自定義作用域的說明,請參閱 使用自定義作用域。 |
單例作用域
只管理一個單例 bean 的共享例項,所有對與該 bean 定義匹配的 ID 的 bean 的請求都會導致 Spring 容器返回該一個特定的 bean 例項。
換句話說,當你定義一個 bean 定義並將其作用域設定為單例時,Spring IoC 容器會精確地建立由該 bean 定義定義的物件的單個例項。這個單個例項儲存在此類單例 bean 的快取中,所有後續對該命名 bean 的請求和引用都返回快取的物件。下圖顯示了單例作用域的工作原理
Spring 對單例 bean 的概念與設計模式(Gang of Four (GoF) patterns)書中定義的單例模式不同。GoF 單例硬編碼了物件的範圍,使得每個 ClassLoader 只建立一個特定類的例項。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"/>
原型作用域
非單例原型 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 生命週期詳細資訊,請參閱 生命週期回撥。)
單例 Bean 與原型 Bean 的依賴關係
當你在單例作用域 bean 中使用原型 bean 依賴項時,請注意依賴項是在例項化時解析的。因此,如果你將一個原型作用域 bean 依賴注入到單例作用域 bean 中,一個新的原型 bean 將被例項化,然後依賴注入到單例 bean 中。原型例項是唯一提供給單例作用域 bean 的例項。
然而,假設你希望單例作用域 bean 在執行時反覆獲取原型作用域 bean 的新例項。你不能將原型作用域 bean 依賴注入到你的單例 bean 中,因為這種注入只發生一次,即在 Spring 容器例項化單例 bean 並解析和注入其依賴項時。如果你需要在執行時多次獲取原型 bean 的新例項,請參閱 方法注入。
請求、會話、應用和 WebSocket 作用域
request、session、application 和 websocket 作用域僅在你使用支援 web 的 Spring ApplicationContext 實現(例如 XmlWebApplicationContext)時可用。如果你將這些作用域與常規 Spring IoC 容器(例如 ClassPathXmlApplicationContext)一起使用,則會丟擲 IllegalStateException,抱怨未知 bean 作用域。
初始 Web 配置
為了支援 request、session、application 和 websocket 級別的 bean 作用域(Web 作用域 bean),在定義 bean 之前需要進行一些簡單的初始配置。(對於標準作用域:singleton 和 prototype,不需要此初始設定。)
如何完成此初始設定取決於你特定的 Servlet 環境。
如果你在 Spring Web MVC 中(實際上是在 Spring DispatcherServlet 處理的請求中)訪問作用域 bean,則無需特殊設定。DispatcherServlet 已經公開了所有相關狀態。
如果你使用 Servlet Web 容器,並且請求在 Spring 的 DispatcherServlet 之外處理(例如,使用 JSF 時),則需要註冊 org.springframework.web.context.request.RequestContextListener ServletRequestListener。這可以透過使用 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 在呼叫鏈中進一步可用。
請求作用域
考慮以下用於 bean 定義的 XML 配置
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring 容器為每個 HTTP 請求使用 loginAction bean 定義建立一個新的 LoginAction bean 例項。也就是說,loginAction bean 的作用域在 HTTP 請求級別。你可以隨意更改建立的例項的內部狀態,因為從同一 loginAction bean 定義建立的其他例項不會看到這些狀態更改。它們是特定於單個請求的。當請求完成處理時,限定到請求的 bean 將被丟棄。
使用註解驅動元件或 Java 配置時,可以使用 @RequestScope 註解將元件分配給 request 作用域。以下示例展示瞭如何實現:
-
Java
-
Kotlin
@RequestScope
@Component
public class LoginAction {
// ...
}
@RequestScope
@Component
class LoginAction {
// ...
}
會話作用域
考慮以下用於 bean 定義的 XML 配置
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring 容器將使用 userPreferences bean 定義為單個 HTTP Session 的生命週期建立一個新的 UserPreferences bean 例項。換句話說,userPreferences bean 的作用域實際上是 HTTP Session 級別。與請求作用域 bean 一樣,你可以隨意更改建立的例項的內部狀態,因為你知道其他 HTTP Session 例項(它們也使用從同一 userPreferences bean 定義建立的例項)不會看到這些狀態更改,因為它們是特定於單個 HTTP Session 的。當 HTTP Session 最終被丟棄時,限定到該特定 HTTP Session 的 bean 也將被丟棄。
使用註解驅動元件或 Java 配置時,可以使用 @SessionScope 註解將元件分配給 session 作用域。
-
Java
-
Kotlin
@SessionScope
@Component
public class UserPreferences {
// ...
}
@SessionScope
@Component
class UserPreferences {
// ...
}
應用作用域
考慮以下用於 bean 定義的 XML 配置
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring 容器為整個 Web 應用程式使用 appPreferences bean 定義一次性建立一個新的 AppPreferences bean 例項。也就是說,appPreferences bean 的作用域在 ServletContext 級別,並作為常規的 ServletContext 屬性儲存。這有點類似於 Spring 單例 bean,但在兩個重要方面有所不同:它是一個 ServletContext 的單例,而不是每個 Spring ApplicationContext 的單例(在任何給定的 Web 應用程式中可能存在多個 ApplicationContext),並且它實際上是暴露的,因此作為 ServletContext 屬性可見。
使用註解驅動元件或 Java 配置時,可以使用 @ApplicationScope 註解將元件分配給 application 作用域。以下示例展示瞭如何實現:
-
Java
-
Kotlin
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
@ApplicationScope
@Component
class AppPreferences {
// ...
}
WebSocket 作用域
WebSocket 作用域與 WebSocket 會話的生命週期相關聯,適用於基於 WebSocket 的 STOMP 應用程式,詳情請參閱 WebSocket 作用域。
作為依賴項的作用域 Bean
Spring IoC 容器不僅管理物件的例項化 (beans),還管理協作者(或依賴項)的連線。如果你想將(例如)一個 HTTP 請求作用域的 bean 注入到另一個生命週期更長的作用域的 bean 中,你可以選擇注入一個 AOP 代理來代替作用域 bean。也就是說,你需要注入一個代理物件,該物件公開與作用域物件相同的公共介面,但也可以從相關作用域(例如 HTTP 請求)檢索真實的目標物件並將方法呼叫委託給真實物件。
|
你還可以在作用域為 當對作用域為 此外,作用域代理並非以生命週期安全的方式訪問短作用域 bean 的唯一方法。你還可以將注入點(即建構函式或 setter 引數或自動裝配欄位)宣告為 作為一種擴充套件變體,你可以宣告 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/> (1)
</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>
| 1 | 定義代理的那一行。 |
要建立這樣一個代理,你需要在一個作用域 bean 定義中插入一個子 <aop:scoped-proxy/> 元素(請參閱選擇要建立的代理型別和基於 XML Schema 的配置)。
為什麼在常見場景中,在 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 Session 作用域 bean (userPreferences) 的引用。這裡的關鍵點是 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 中,而 userManager bean 並不知道此 UserPreferences 引用是一個代理。在此示例中,當 UserManager 例項呼叫依賴注入的 UserPreferences 物件上的方法時,它實際上是在呼叫代理上的方法。然後,代理從(在此情況下)HTTP Session 中獲取真實的 UserPreferences 物件,並將方法呼叫委託給檢索到的真實 UserPreferences 物件。
因此,當將 request- 和 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 容器為帶有 <aop:scoped-proxy/> 元素標記的 bean 建立代理時,會建立一個基於 CGLIB 的類代理。
|
CGLIB 代理不會攔截私有方法。嘗試在此類代理上呼叫私有方法不會委託給實際的作用域目標物件。 |
或者,你可以透過將 <aop:scoped-proxy/> 元素的 proxy-target-class 屬性值指定為 false,來配置 Spring 容器為這些作用域 bean 建立基於標準 JDK 介面的代理。使用基於 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>
有關選擇基於類或基於介面的代理的更詳細資訊,請參閱代理機制。
自定義作用域
Bean 作用域機制是可擴充套件的。你可以定義自己的作用域,甚至可以重新定義現有作用域,儘管後者被認為是糟糕的做法,並且你無法覆蓋內建的 singleton 和 prototype 作用域。
建立自定義作用域
要將你的自定義作用域整合到 Spring 容器中,你需要實現 org.springframework.beans.factory.config.Scope 介面,本節將對此進行描述。為了瞭解如何實現你自己的作用域,請參閱 Spring 框架本身提供的 Scope 實現以及 Scope 的 Javadoc,其中更詳細地解釋了你需要實現的方法。
Scope 介面有四種方法:從作用域中獲取物件、從作用域中刪除物件以及讓它們被銷燬。
例如,會話作用域實現返回會話作用域的 bean(如果不存在,該方法會返回一個新的 bean 例項,並在將其繫結到會話以供將來引用)。以下方法從底層作用域返回物件
-
Java
-
Kotlin
Object get(String name, ObjectFactory<?> objectFactory)
fun get(name: String, objectFactory: ObjectFactory<*>): Any
例如,會話作用域實現將從底層會話中刪除會話作用域的 bean。物件應該被返回,但如果找不到指定名稱的物件,你可以返回 null。以下方法從底層作用域中刪除物件
-
Java
-
Kotlin
Object remove(String name)
fun remove(name: String): Any
以下方法註冊一個回撥,當作用域被銷燬或作用域中指定物件被銷燬時,作用域應呼叫此回撥
-
Java
-
Kotlin
void registerDestructionCallback(String name, Runnable destructionCallback)
fun registerDestructionCallback(name: String, destructionCallback: Runnable)
有關銷燬回撥的更多資訊,請參閱 Javadoc 或 Spring 作用域實現。
以下方法獲取底層作用域的會話識別符號
-
Java
-
Kotlin
String getConversationId()
fun getConversationId(): String
此識別符號對每個作用域都不同。對於會話作用域實現,此識別符號可以是會話識別符號。
使用自定義作用域
在編寫並測試了一個或多個自定義 Scope 實現之後,你需要讓 Spring 容器知道你的新作用域。以下方法是向 Spring 容器註冊新 Scope 的核心方法
-
Java
-
Kotlin
void registerScope(String scopeName, Scope scope);
fun registerScope(scopeName: String, scope: Scope)
此方法在 ConfigurableBeanFactory 介面上宣告,可透過 Spring 附帶的大多數具體 ApplicationContext 實現上的 BeanFactory 屬性獲得。
registerScope(..) 方法的第一個引數是與作用域關聯的唯一名稱。Spring 容器本身中的此類名稱示例包括 singleton 和 prototype。registerScope(..) 方法的第二個引數是你希望註冊和使用的自定義 Scope 實現的實際例項。
假設你編寫了自定義 Scope 實現,然後按以下示例進行註冊。
下一個示例使用 SimpleThreadScope,它包含在 Spring 中,但預設情況下未註冊。你自己的自定義 Scope 實現的說明也是相同的。 |
-
Java
-
Kotlin
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
val threadScope = SimpleThreadScope()
beanFactory.registerScope("thread", threadScope)
然後,你可以建立符合自定義 Scope 作用域規則的 bean 定義,如下所示
<bean id="..." class="..." scope="thread">
使用自定義 Scope 實現,你不僅限於透過程式設計方式註冊作用域。你還可以透過使用 CustomScopeConfigurer 類以宣告方式進行 Scope 註冊,如以下示例所示
<?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 實現的 <bean> 宣告中放置 <aop:scoped-proxy/> 時,是工廠 bean 本身具有作用域,而不是 getObject() 返回的物件。 |