Bean 的作用域

當您建立 Bean 定義時,您就建立了一個配方,用於建立由該 Bean 定義所定義之類別的實際實例。Bean 定義是一個配方的概念很重要,因為這表示,如同類別一樣,您可以從單一配方建立許多物件實例。

您不僅可以控制將插入從特定 Bean 定義建立之物件的各種相依性和組態值,還可以控制從特定 Bean 定義建立之物件的作用域。這種方法既強大又靈活,因為您可以透過組態選擇要建立之物件的作用域,而不必將物件的作用域烘烤到 Java 類別層級中。Bean 可以定義為部署在多個作用域之一中。Spring 框架支援六個作用域,其中四個作用域僅在您使用具備 Web 感知的 ApplicationContext 時才可用。您也可以建立 自訂作用域。

下表描述了支援的作用域

表 1. Bean 作用域
作用域 描述

singleton

(預設) 將單一 Bean 定義的作用域設定為每個 Spring IoC 容器的單一物件實例。

prototype

將單一 Bean 定義的作用域設定為任意數量的物件實例。

request

將單一 Bean 定義的作用域設定為單一 HTTP 請求的生命週期。也就是說,每個 HTTP 請求都有其自己的 Bean 實例,該實例是根據單一 Bean 定義建立的。僅在具備 Web 感知的 Spring ApplicationContext 的 Context 中有效。

session

將單一 Bean 定義的作用域設定為 HTTP Session 的生命週期。僅在具備 Web 感知的 Spring ApplicationContext 的 Context 中有效。

application

將單一 Bean 定義的作用域設定為 ServletContext 的生命週期。僅在具備 Web 感知的 Spring ApplicationContext 的 Context 中有效。

websocket

將單一 Bean 定義的作用域設定為 WebSocket 的生命週期。僅在具備 Web 感知的 Spring ApplicationContext 的 Context 中有效。

有一個執行緒作用域可用,但預設未註冊。如需詳細資訊,請參閱 SimpleThreadScope 的文件。如需有關如何註冊此作用域或任何其他自訂作用域的指示,請參閱 使用自訂作用域

Singleton 作用域

只管理 singleton Bean 的一個共用實例,並且對 ID 或 IDs 與該 Bean 定義相符的 Bean 的所有請求,都會導致 Spring 容器傳回該特定 Bean 實例。

換句話說,當您定義 Bean 定義並將其作用域設定為 singleton 時,Spring IoC 容器會精確地建立一個由該 Bean 定義所定義之物件的實例。此單一實例會儲存在此類 singleton Bean 的快取中,並且後續對該具名 Bean 的所有請求和參考都會傳回快取物件。下圖顯示 singleton 作用域的工作方式

singleton

Spring 的 singleton Bean 概念與四人幫 (GoF) 模式書中定義的 singleton 模式不同。GoF singleton 硬式編碼物件的作用域,以便每個 ClassLoader 只建立一個特定類別的實例。Spring singleton 的作用域最好描述為每個容器和每個 Bean。這表示,如果您在單一 Spring 容器中為特定類別定義一個 Bean,則 Spring 容器會建立一個且僅一個由該 Bean 定義所定義之類別的實例。singleton 作用域是 Spring 中的預設作用域。若要在 XML 中將 Bean 定義為 singleton,您可以定義一個 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"/>

Prototype 作用域

Bean 部署的非 singleton prototype 作用域會在每次對該特定 Bean 發出請求時,建立新的 Bean 實例。也就是說,Bean 會注入到另一個 Bean 中,或者您透過容器上的 getBean() 方法呼叫來請求它。作為規則,您應該將 prototype 作用域用於所有具狀態的 Bean,並將 singleton 作用域用於無狀態的 Bean。

下圖說明 Spring prototype 作用域

prototype

(資料存取物件 (DAO) 通常不會組態為 prototype,因為典型的 DAO 不會保留任何對話狀態。我們更容易重複使用 singleton 圖的核心。)

下列範例在 XML 中將 Bean 定義為 prototype

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

與其他作用域相反,Spring 不管理 prototype Bean 的完整生命週期。容器會實例化、組態並以其他方式組裝 prototype 物件,並將其交給用戶端,而不會再記錄該 prototype 實例。因此,儘管初始化生命週期回呼方法會在所有物件上呼叫,而與作用域無關,但在 prototype 的情況下,不會呼叫組態的銷毀生命週期回呼。用戶端程式碼必須清除 prototype 作用域的物件,並釋放 prototype Bean 保有的昂貴資源。若要讓 Spring 容器釋放 prototype 作用域 Bean 保有的資源,請嘗試使用自訂 Bean 後處理器,該後處理器會保留對需要清除之 Bean 的參考。

在某些方面,Spring 容器在 prototype 作用域 Bean 方面的作用是 Java new 運算子的替代品。該點之後的所有生命週期管理都必須由用戶端處理。(有關 Spring 容器中 Bean 的生命週期的詳細資訊,請參閱 生命週期回呼。)

具有 Prototype Bean 相依性的 Singleton Bean

當您使用 singleton 作用域的 Bean 並相依於 prototype Bean 時,請注意相依性是在實例化時解析的。因此,如果您將 prototype 作用域的 Bean 相依性注入到 singleton 作用域的 Bean 中,則會實例化新的 prototype Bean,然後將其相依性注入到 singleton Bean 中。prototype 實例是唯一提供給 singleton 作用域 Bean 的實例。

但是,假設您希望 singleton 作用域的 Bean 在執行階段重複取得 prototype 作用域 Bean 的新實例。您無法將 prototype 作用域的 Bean 相依性注入到您的 singleton Bean 中,因為該注入只會發生一次,即當 Spring 容器實例化 singleton Bean 並解析和注入其相依性時。如果您在執行階段需要 prototype Bean 的新實例超過一次,請參閱 方法注入

Request、Session、Application 和 WebSocket 作用域

只有在使用具備 Web 感知的 Spring ApplicationContext 實作 (例如 XmlWebApplicationContext) 時,requestsessionapplicationwebsocket 作用域才可用。如果您將這些作用域與一般 Spring IoC 容器 (例如 ClassPathXmlApplicationContext) 搭配使用,則會擲回 IllegalStateException,抱怨 Bean 作用域不明。

初始 Web 組態

若要支援在 requestsessionapplicationwebsocket 層級 (Web 作用域 Bean) 設定 Bean 的作用域,在定義 Bean 之前,需要進行一些小的初始組態。(標準作用域 singletonprototype 不需要此初始設定。)

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

如果您在 Spring Web MVC 中存取作用域 Bean,實際上是在 Spring DispatcherServlet 處理的請求中存取,則不需要特殊設定。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>

DispatcherServletRequestContextListenerRequestContextFilter 都做完全相同的事情,即將 HTTP 請求物件繫結到正在處理該請求的 Thread。這使得作用域為請求和 Session 的 Bean 在呼叫鏈中更下游的位置可用。

Request 作用域

考慮下列 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 {
	// ...
}

Session 作用域

考慮下列 Bean 定義的 XML 組態

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

Spring 容器針對單一 HTTP Session 的生命週期,使用 userPreferences Bean 定義建立 UserPreferences Bean 的新實例。換句話說,userPreferences Bean 的作用域實際上設定在 HTTP Session 層級。與請求作用域的 Bean 一樣,您可以根據需要變更所建立實例的內部狀態,因為也使用從相同的 userPreferences Bean 定義建立之實例的其他 HTTP Session 實例看不到這些狀態變更,因為它們特定於個別 HTTP Session。當 HTTP Session 最終被捨棄時,作用域設定為該特定 HTTP Session 的 Bean 也會被捨棄。

當使用註解驅動的元件或 Java 組態時,您可以使用 @SessionScope 註解將元件指派給 session 作用域。

  • Java

  • Kotlin

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

Application 作用域

考慮下列 Bean 定義的 XML 組態

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

Spring 容器針對整個 Web 應用程式,使用 appPreferences Bean 定義建立 AppPreferences Bean 的新實例一次。也就是說,appPreferences Bean 的作用域設定在 ServletContext 層級,並儲存為一般 ServletContext 屬性。這有點類似於 Spring singleton Bean,但在兩個重要方面有所不同:它是每個 ServletContext 的 singleton,而不是每個 Spring ApplicationContext 的 singleton (在任何給定的 Web 應用程式中,可能有多個 Spring ApplicationContext),並且它實際上是公開的,因此可作為 ServletContext 屬性看到。

當使用註解驅動的元件或 Java 組態時,您可以使用 @ApplicationScope 註解將元件指派給 application 作用域。下列範例顯示如何執行此操作

  • Java

  • Kotlin

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

WebSocket 作用域

WebSocket scope 與 WebSocket 工作階段的生命週期相關聯,並適用於基於 WebSocket 的 STOMP 應用程式,詳情請參閱WebSocket scope

作為依賴項的具作用域 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>,它提供多種額外的存取變體,包括 getIfAvailablegetIfUnique

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

	<!-- 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 定義代理的行。

要建立這樣的代理,您需要將子元素 <aop:scoped-proxy/> 插入到具作用域的 bean 定義中(請參閱選擇要建立的代理類型基於 XML Schema 的組態)。

為什麼在常見情境中,作用域為 requestsession 和自訂作用域層級的 bean 定義需要 <aop:scoped-proxy/> 元素?請考慮以下 singleton 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>

在前面的範例中,singleton bean (userManager) 被注入了對 HTTP Session 作用域 bean (userPreferences) 的參考。這裡的重點是 userManager bean 是一個 singleton:它在每個容器中僅實例化一次,並且它的依賴項(在本例中只有一個,即 userPreferences bean)也僅注入一次。這表示 userManager bean 僅在完全相同的 userPreferences 物件上運作(即最初注入的物件)。

當將生命週期較短的作用域 bean 注入到生命週期較長的作用域 bean 中時(例如,將 HTTP Session 作用域的協作 bean 作為依賴項注入到 singleton bean 中),這不是您想要的行為。相反,您需要一個單一的 userManager 物件,並且在 HTTP Session 的生命週期內,您需要一個特定於 HTTP SessionuserPreferences 物件。因此,容器會建立一個物件,該物件公開與 UserPreferences 類別完全相同的公共介面(理想情況下,物件是 UserPreferences 實例),它可以從作用域機制(HTTP 請求、Session 等)中取得真實的 UserPreferences 物件。容器將此代理物件注入到 userManager bean 中,而 userManager 並不知道此 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 代理不會攔截私有方法。嘗試在這樣的代理上調用私有方法將不會委派給實際的具作用域目標物件。

或者,您可以將 Spring 容器組態為為此類具作用域的 bean 建立標準的基於 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>

有關選擇基於類別或基於介面的代理的更多詳細資訊,請參閱代理機制

直接注入 Request/Session 參考

作為 factory scope 的替代方案,Spring WebApplicationContext 也支援將 HttpServletRequestHttpServletResponseHttpSessionWebRequest 以及(如果存在 JSF)FacesContextExternalContext 注入到 Spring 管理的 bean 中,只需通過基於類型的自動裝配以及其他 bean 的常規注入點即可。Spring 通常為此類請求和會話物件注入代理,這具有在 singleton bean 和可序列化 bean 中工作的優勢,類似於 factory-scoped bean 的作用域代理。

自訂作用域

bean 作用域機制是可擴展的。您可以定義自己的作用域,甚至重新定義現有的作用域,儘管後者被認為是不良做法,並且您無法覆寫內建的 singletonprototype 作用域。

建立自訂作用域

要將您的自訂作用域整合到 Spring 容器中,您需要實作 org.springframework.beans.factory.config.Scope 介面,本節將對其進行說明。有關如何實作您自己的作用域的想法,請參閱 Spring Framework 本身提供的 Scope 實作以及 Scope javadoc,其中更詳細地說明了您需要實作的方法。

Scope 介面有四個方法,用於從作用域取得物件、從作用域中移除物件以及讓它們被銷毀。

例如,session 作用域實作會傳回 session 作用域的 bean(如果它不存在,則該方法會傳回 bean 的新實例,並在將其綁定到 session 以供將來參考之後)。以下方法從底層作用域傳回物件

  • Java

  • Kotlin

Object get(String name, ObjectFactory<?> objectFactory)
fun get(name: String, objectFactory: ObjectFactory<*>): Any

例如,session 作用域實作會從底層 session 中移除 session 作用域的 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

對於每個作用域,此識別符都不同。對於 session 作用域實作,此識別符可以是 session 識別符。

使用自訂作用域

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

  • Java

  • Kotlin

void registerScope(String scopeName, Scope scope);
fun registerScope(scopeName: String, scope: Scope)

此方法在 ConfigurableBeanFactory 介面上宣告,該介面可通過 Spring 隨附的大多數具體 ApplicationContext 實作上的 BeanFactory 屬性取得。

registerScope(..) 方法的第一個引數是與作用域關聯的唯一名稱。Spring 容器本身中的此類名稱範例為 singletonprototyperegisterScope(..) 方法的第二個引數是您希望註冊和使用的自訂 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>
當您將 <aop:scoped-proxy/> 放在 FactoryBean 實作的 <bean> 宣告中時,作用域是 factory bean 本身,而不是從 getObject() 傳回的物件。