Bean 的作用域
當您建立 Bean 定義時,您就建立了一個配方,用於建立由該 Bean 定義所定義之類別的實際實例。Bean 定義是一個配方的概念很重要,因為這表示,如同類別一樣,您可以從單一配方建立許多物件實例。
您不僅可以控制將插入從特定 Bean 定義建立之物件的各種相依性和組態值,還可以控制從特定 Bean 定義建立之物件的作用域。這種方法既強大又靈活,因為您可以透過組態選擇要建立之物件的作用域,而不必將物件的作用域烘烤到 Java 類別層級中。Bean 可以定義為部署在多個作用域之一中。Spring 框架支援六個作用域,其中四個作用域僅在您使用具備 Web 感知的 ApplicationContext
時才可用。您也可以建立 自訂作用域。
下表描述了支援的作用域
作用域 | 描述 |
---|---|
(預設) 將單一 Bean 定義的作用域設定為每個 Spring IoC 容器的單一物件實例。 |
|
將單一 Bean 定義的作用域設定為任意數量的物件實例。 |
|
將單一 Bean 定義的作用域設定為單一 HTTP 請求的生命週期。也就是說,每個 HTTP 請求都有其自己的 Bean 實例,該實例是根據單一 Bean 定義建立的。僅在具備 Web 感知的 Spring |
|
將單一 Bean 定義的作用域設定為 HTTP |
|
將單一 Bean 定義的作用域設定為 |
|
將單一 Bean 定義的作用域設定為 |
有一個執行緒作用域可用,但預設未註冊。如需詳細資訊,請參閱 SimpleThreadScope 的文件。如需有關如何註冊此作用域或任何其他自訂作用域的指示,請參閱 使用自訂作用域。 |
Singleton 作用域
只管理 singleton Bean 的一個共用實例,並且對 ID 或 IDs 與該 Bean 定義相符的 Bean 的所有請求,都會導致 Spring 容器傳回該特定 Bean 實例。
換句話說,當您定義 Bean 定義並將其作用域設定為 singleton 時,Spring IoC 容器會精確地建立一個由該 Bean 定義所定義之物件的實例。此單一實例會儲存在此類 singleton Bean 的快取中,並且後續對該具名 Bean 的所有請求和參考都會傳回快取物件。下圖顯示 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 作用域

(資料存取物件 (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
) 時,request
、session
、application
和 websocket
作用域才可用。如果您將這些作用域與一般 Spring IoC 容器 (例如 ClassPathXmlApplicationContext
) 搭配使用,則會擲回 IllegalStateException
,抱怨 Bean 作用域不明。
初始 Web 組態
若要支援在 request
、session
、application
和 websocket
層級 (Web 作用域 Bean) 設定 Bean 的作用域,在定義 Bean 之前,需要進行一些小的初始組態。(標準作用域 singleton
和 prototype
不需要此初始設定。)
您如何完成此初始設定取決於您的特定 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>
DispatcherServlet
、RequestContextListener
和 RequestContextFilter
都做完全相同的事情,即將 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 請求)檢索真實的目標物件,並將方法呼叫委派給真實物件。
您也可以在作用域為 當針對作用域為 此外,作用域代理並不是以生命週期安全的方式存取來自較短作用域 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 | 定義代理的行。 |
要建立這樣的代理,您需要將子元素 <aop:scoped-proxy/>
插入到具作用域的 bean 定義中(請參閱選擇要建立的代理類型和基於 XML Schema 的組態)。
為什麼在常見情境中,作用域為 request
、session
和自訂作用域層級的 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 Session
的 userPreferences
物件。因此,容器會建立一個物件,該物件公開與 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
也支援將 HttpServletRequest
、HttpServletResponse
、HttpSession
、WebRequest
以及(如果存在 JSF)FacesContext
和 ExternalContext
注入到 Spring 管理的 bean 中,只需通過基於類型的自動裝配以及其他 bean 的常規注入點即可。Spring 通常為此類請求和會話物件注入代理,這具有在 singleton bean 和可序列化 bean 中工作的優勢,類似於 factory-scoped bean 的作用域代理。
自訂作用域
bean 作用域機制是可擴展的。您可以定義自己的作用域,甚至重新定義現有的作用域,儘管後者被認為是不良做法,並且您無法覆寫內建的 singleton
和 prototype
作用域。
建立自訂作用域
要將您的自訂作用域整合到 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 容器本身中的此類名稱範例為 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>
當您將 <aop:scoped-proxy/> 放在 FactoryBean 實作的 <bean> 宣告中時,作用域是 factory bean 本身,而不是從 getObject() 傳回的物件。 |