Bean 作用域
當你建立一個 bean 定義時,你實際上是為建立該 bean 定義所定義的類的實際例項建立了一份“菜譜”。將 bean 定義看作一份“菜譜”是很重要的,因為這意味著,就像類一樣,你可以從一份“菜譜”創建出許多物件例項。
你不僅可以控制要注入到從特定 bean 定義建立的物件中的各種依賴項和配置值,還可以控制從特定 bean 定義建立的物件的“作用域”(scope)。這種方法強大且靈活,因為你可以透過配置來選擇建立物件的“作用域”,而不必在 Java 類級別中硬編碼物件的“作用域”。可以定義 bean 以部署在多種作用域中的一種。Spring Framework 支援六種作用域,其中四種只有在使用支援 Web 的 ApplicationContext
時才可用。你還可以建立自定義作用域。
下表描述了支援的作用域
作用域 | 描述 |
---|---|
(預設)將單個 bean 定義限定到每個 Spring IoC 容器中的單個物件例項。 |
|
將單個 bean 定義限定到任意數量的物件例項。 |
|
將單個 bean 定義限定到單個 HTTP 請求的生命週期。也就是說,每個 HTTP 請求都擁有基於該單個 bean 定義建立的自己的 bean 例項。僅在支援 Web 的 Spring |
|
將單個 bean 定義限定到 HTTP |
|
將單個 bean 定義限定到 |
|
將單個 bean 定義限定到 |
雖然提供了 thread 作用域,但預設不註冊。更多資訊,請參閱 SimpleThreadScope 的文件。關於如何註冊此作用域或任何其他自定義作用域的說明,請參閱 使用自定義作用域。 |
Singleton 作用域
Spring 容器只管理一個共享的 singleton bean 例項,所有對 ID 或與該 bean 定義匹配的 ID 的 bean 的請求都將返回該特定的 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 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 作用域
非 singleton 的 prototype bean 部署作用域使得每次請求該特定 bean 時都會建立一個新的 bean 例項。也就是說,該 bean 被注入到另一個 bean 中,或者你透過容器上的 getBean()
方法呼叫請求它。通常,你應該對所有有狀態 bean 使用 prototype 作用域,對無狀態 bean 使用 singleton 作用域。
下圖說明了 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
當你使用依賴於 prototype bean 的 singleton 作用域 bean 時,請注意依賴項在例項化時解析。因此,如果你將一個 prototype 作用域的 bean 依賴注入到一個 singleton 作用域的 bean 中,Spring 會例項化一個新的 prototype bean,然後將其依賴注入到 singleton bean 中。這個 prototype 例項是唯一一個提供給該 singleton 作用域 bean 的例項。
然而,假設你希望 singleton 作用域的 bean 在執行時重複地獲取 prototype 作用域 bean 的新例項。你不能直接將 prototype 作用域的 bean 依賴注入到你的 singleton bean 中,因為這種注入只發生一次,即在 Spring 容器例項化 singleton bean 並解析和注入其依賴項時。如果你需要在執行時多次獲取 prototype bean 的新例項,請參閱方法注入。
Request、Session、Application 和 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 中訪問作用域 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 請求物件繫結到正在處理該請求的執行緒。這使得 request 作用域和 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 容器使用 userPreferences
bean 定義為單個 HTTP Session
的生命週期建立一個新的 UserPreferences
bean 例項。換句話說,userPreferences
bean 的作用域實際上是在 HTTP Session
級別。與 request 作用域的 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 作用域與 WebSocket 會話的生命週期相關聯,適用於基於 WebSocket 的 STOMP 應用,詳情請參閱WebSocket 作用域。
作為依賴項的作用域 Bean
Spring IoC 容器不僅管理物件的例項化(bean 的建立),還管理協作物件(或依賴項)的裝配。如果你想將(例如)HTTP request 作用域的 bean 注入到具有更長生命週期的另一個 bean 中,你可以選擇注入一個 AOP 代理來替代該作用域 bean。也就是說,你需要注入一個代理物件,該物件暴露與作用域物件相同的公共介面,並且能夠從相關作用域(例如 HTTP 請求)檢索真實的 target 物件,並將方法呼叫委託給真實的 target 物件。
你也可以在 singleton 作用域的 bean 之間使用 當針對 prototype 作用域的 bean 宣告 此外,作用域代理並不是以生命週期安全的方式訪問來自較短作用域 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
bean 並不知道這個 UserPreferences
引用實際上是一個代理。在這個示例中,當 UserManager
例項呼叫依賴注入的 UserPreferences
物件上的一個方法時,它實際上是呼叫了代理上的方法。然後,代理會從(在這種情況下)HTTP Session
中獲取真實的 UserPreferences
物件,並將方法呼叫委託給獲取到的真實的 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 介面的代理意味著你的應用程式 classpath 中不需要額外的庫來影響此類代理。然而,這也意味著作用域 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>
有關選擇基於類或基於介面的代理的更多詳細資訊,請參閱 代理機制。
直接注入請求/會話引用
作為工廠作用域的替代方案,Spring WebApplicationContext
還支援將 HttpServletRequest
、HttpServletResponse
、HttpSession
、WebRequest
以及(如果存在 JSF)FacesContext
和 ExternalContext
注入到 Spring 管理的 bean 中,只需透過基於型別的自動裝配即可,就像注入其他 bean 的常規注入點一樣。Spring 通常為這些請求和會話物件注入代理,這具有在 singleton bean 和可序列化 bean 中也能工作的優點,類似於工廠作用域 bean 的作用域代理。
自定義作用域
bean 的作用域機制是可擴充套件的。你可以定義自己的作用域,甚至可以重新定義現有的作用域,儘管後者被認為是不良實踐,並且你不能覆蓋內建的 singleton
和 prototype
作用域。
建立自定義作用域
要將你的自定義作用域整合到 Spring 容器中,你需要實現本節中描述的 org.springframework.beans.factory.config.Scope
介面。要了解如何實現自己的作用域,請參閱 Spring Framework 自身提供的 Scope
實現以及 Scope
javadoc,其中更詳細地解釋了你需要實現的方法。
Scope
介面有四個方法,用於從作用域獲取物件、從作用域移除物件以及讓它們被銷燬。
例如,session 作用域實現返回 session 作用域的 bean(如果不存在,該方法在將其繫結到 session 以供將來引用後,返回 bean 的新例項)。以下方法從底層作用域返回物件
-
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>
當你在 FactoryBean 實現的 <bean> 宣告中放置 <aop:scoped-proxy/> 時,被賦予作用域的是工廠 bean 本身,而不是從 getObject() 返回的物件。 |