Bean 作用域
当你创建一个 bean 定义时,你实际上是为该 bean 定义所定义的类创建实际实例提供了一个“配方”。将 bean 定义视为“配方”这一理念非常重要,因为它意味着,就像一个类一样,你可以从单个配方中创建多个对象实例。
你不仅可以控制要插入从特定 bean 定义创建的对象的各种依赖项和配置值,还可以控制从特定 bean 定义创建的对象的范围。这种方法功能强大且灵活,因为你可以通过配置来选择对象的范围,而不必在 Java 类级别硬编码对象的范围。Bean 可以被定义为部署在多种作用域中的一种。
Spring Framework 支持六种作用域,其中四种仅在你使用支持 Web 的 ApplicationContext
时可用。你还可以创建 自定义作用域。
下表描述了支持的作用域:
作用域 | 描述 |
---|---|
(默认) 将单个 bean 定义限定为每个 Spring IoC 容器的一个对象实例。 |
|
将单个 bean 定义限定为任意数量的对象实例。 |
|
将单个 bean 定义限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有其自己的基于单个 bean 定义创建的 bean 实例。仅在支持 Web 的 Spring |
|
将单个 bean 定义限定为 HTTP |
|
将单个 bean 定义限定为 |
|
将单个 bean 定义限定为 |
线程作用域可用,但默认情况下未注册。有关更多信息,请参阅 |
单例作用域
只管理一个共享的单例 bean 实例,所有对与该 bean 定义匹配的 ID 或 ID 的 bean 的请求都会导致 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:
<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
级别(Web 作用域 bean)的 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 容器使用 loginAction
bean 定义为每个 HTTP 请求创建一个新的 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 一样,你可以随意更改所创建实例的内部状态,因为从同一个 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 {
// ...
}
应用作用域
考虑以下 bean 定义的 XML 配置:
<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
作用域。以下示例展示了如何实现:
-
Java
-
Kotlin
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
@ApplicationScope
@Component
class AppPreferences {
// ...
}
WebSocket 作用域
WebSocket 作用域与 WebSocket 会话的生命周期相关联,并适用于基于 STOMP over WebSocket 的应用程序,详见 WebSocket 作用域。
作用域 Bean 作为依赖项
Spring IoC 容器不仅管理对象的实例化(bean),还管理协作者(或依赖项)的连接。如果你想将(例如)HTTP 请求作用域的 bean 注入到另一个生命周期更长的作用域的 bean 中,你可能会选择注入一个 AOP 代理来替代作用域 bean。也就是说,你需要注入一个代理对象,该对象暴露与作用域对象相同的公共接口,但也可以从相关作用域(例如 HTTP 请求)检索真正的目标对象,并将方法调用委托给真正的对象。
你也可以在作用域为 |
以下示例中的配置只有一行,但理解其背后的 “为什么” 以及 “如何” 非常重要:
<?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/> [id="CO1-1"]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 Framework 自身提供的 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
实现,然后按以下示例所示注册它。
下一个示例使用 |
-
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>
当你在 |