Bean Scopes
当你创建一个bean定义时,其实就是在为创建该bean定义所定义类的实际实例制定一份“配方”。将bean定义视为一份“配方”的观点非常重要,因为这意味着,就像创建一个类一样,你可以根据同一份“配方”创建多个对象实例。
你不仅可以控制要插入到从特定bean定义创建的对象中的各种依赖关系和配置值,还可以控制从该bean定义创建的对象的作用域。这种方法是强大且灵活的,因为你可以通过配置来选择创建的对象的作用域,而无需在Java类级别就固定对象的作用域。Bean可以被定义为在多种作用域之一中部署。Spring框架支持六种作用域,其中四种只有在使用具有Web感知能力的ApplicationContext时才可用。你还可以创建自定义作用域。
下表描述了所支持的作用域:
表1. Bean作用域
| 作用域 | 描述 |
|---|---|
| singleton | (默认)将单个bean定义的作用域限定为每个Spring IoC容器中的单个对象实例。 |
| prototype | 将单个bean定义的作用域限定为任意数量的对象实例。 |
| request | 将单个bean定义的作用域限定为单个HTTP请求的生命周期。也就是说,每个HTTP请求都会根据该bean定义创建自己的实例。仅适用于支持Web的Spring ApplicationContext上下文。 |
| session | 将单个bean定义的作用域限定为HTTP Session的生命周期。仅适用于支持Web的Spring ApplicationContext上下文。 |
| application | 将单个bean定义的作用域限定为ServletContext的生命周期。仅适用于支持Web的Spring ApplicationContext上下文。 |
| websocket | 将单个bean定义的作用域限定为WebSocket的生命周期。仅适用于支持Web的Spring ApplicationContext上下文。 |
线程作用域(thread scope)是可用的,但默认情况下并未被注册。欲了解更多信息,请参阅SimpleThreadScope的文档。关于如何注册此作用域或任何其他自定义作用域的说明,请参见使用自定义作用域。
单例作用域
只有一个单例 Bean 的实例会被管理,所有针对与该 Bean 定义匹配的 ID 的请求,都会导致 Spring 容器返回那个特定的 Bean 实例。
换句话说,当你定义一个bean时,并将其作用域设置为singleton(单例),Spring IoC容器会创建该bean定义所指定的对象的一个唯一实例。这个单一实例会被存储在一个单例bean的缓存中,之后所有对该bean的请求和引用都会返回这个缓存的对象。下图展示了singleton作用域的工作原理:

Spring中的单例bean概念与《四人组(GoF)模式》一书中所定义的单例模式有所不同。GoF的单例模式将对象的作用域硬编码为:每个ClassLoader只创建一个特定类的实例。而Spring中的单例作用域可以最好地描述为“每个容器、每个bean各自独立”。这意味着,如果你在单个Spring容器中为某个特定类定义了一个bean,那么Spring容器只会创建该bean定义所指定的那个类的一个实例。单例作用域是Spring中的默认作用域。要在XML中定义一个单例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部署的非单例(non-singleton)原型作用域会导致每次请求该特定bean时都会创建一个新的bean实例。也就是说,该bean会被注入到另一个bean中,或者通过调用容器的getBean()方法来获取它。通常情况下,对于有状态(stateful)的bean应该使用原型作用域,而对于无状态(stateless)的bean则应使用单例作用域。
下图说明了Spring的prototype作用域:

(数据访问对象(DAO)通常不会被配置为原型,因为典型的DAO不保存任何会话状态。对我们来说,重用单例图的核心部分更为简单。)
以下示例在XML中将一个bean定义为原型:
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
与其他作用域不同,Spring并不管理原型Bean的完整生命周期。容器负责实例化、配置并组装原型对象,然后将其交给客户端,此后不会再对该原型实例进行任何追踪。因此,尽管所有对象(无论其作用域如何)都会调用初始化生命周期回调方法,但对于原型Bean来说,配置好的销毁生命周期回调方法却不会被调用。客户端代码必须负责清理这些属于原型作用域的对象,并释放原型Bean所持有的昂贵资源。为了让Spring容器释放这些资源,可以尝试使用自定义的Bean后处理器,该后处理器会保存需要被清理的Bean的引用。
在某些方面,Spring容器对于原型作用域(prototype-scoped)bean的作用相当于替代了Java的new运算符。从这一点之后的所有生命周期管理都必须由客户端来处理。(有关Spring容器中bean的生命周期的详细信息,请参见生命周期回调。)
具有Prototype Bean依赖的Singleton 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中,如果你需要访问作用域内的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 上。这样,请求范围(request-scoped)和会话范围(session-scoped)的 Bean 就能够在调用链的后续环节中被使用到。
请求范围
考虑以下用于Bean定义的XML配置:
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring容器在每次HTTP请求时,都会使用loginActionbean定义来创建一个新的LoginActionbean实例。也就是说,loginActionbean的作用域是HTTP请求级别的。你可以随意更改所创建实例的内部状态,因为从同一个loginActionbean定义创建的其他实例并不会看到这些状态变化。这些状态变化仅针对单个请求有效。当请求处理完成时,该请求作用域内的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容器在单个HTTP会话(HTTP Session)的生命周期内,使用userPreferencesbean的定义来创建一个新的UserPreferencesbean实例。换句话说,userPreferencesbean的有效作用范围实际上是在HTTP会话级别。与请求作用域(request-scoped)的bean一样,你可以随意更改所创建实例的内部状态,但需要注意的是,其他也使用从同一userPreferencesbean定义创建的实例的HTTP会话,将不会看到这些状态变化,因为这些状态变化是特定于某个单独的HTTP会话的。当该HTTP会话最终被丢弃时,作用域限定在该特定HTTP会话上的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应用程序中仅使用一次appPreferencesbean定义来创建AppPreferencesbean的新实例。也就是说,appPreferencesbean的作用域是ServletContext级别,并且作为普通的ServletContext属性进行存储。这与Spring的单例bean有些类似,但有两个重要的区别:它是每个ServletContext的单例,而不是每个SpringApplicationContext的单例(因为在任何给定的Web应用程序中可能有多个ApplicationContext),并且它实际上是作为ServletContext属性暴露出来的,因此是可见的。
在使用基于注解的组件或Java配置时,可以使用@ApplicationScope注解将组件分配到application作用域。以下示例展示了如何进行操作:
- Java
- Kotlin
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
@ApplicationScope
@Component
class AppPreferences {
// ...
}
WebSocket Scope
WebSocket作用域(WebSocket scope)与WebSocket会话的生命周期相关联,也适用于基于WebSocket的STOMP应用。有关更多详细信息,请参阅WebSocket作用域。
作为依赖的限定作用域Bean
Spring的IoC容器不仅负责对象的实例化(即Bean的创建),还负责管理对象之间的依赖关系(即协作关系的建立)。例如,如果你想将一个仅在HTTP请求范围内有效的Bean注入到生命周期更长的Bean中,你可以选择注入一个AOP代理来代替那个短生命周期的Bean。也就是说,你需要注入一个代理对象,这个代理对象需要暴露与被注入Bean相同的公共接口,同时它还能够从相应的作用域(如HTTP请求)中获取到实际的被注入对象,并将方法调用委托给实际的被注入对象执行。
你也可以在被定义为 singleton 的bean之间使用 <aop:scoped-proxy/>,这样引用就会通过一个可序列化的中间代理,因此在反序列化时能够重新获取到目标的单例bean。
当对范围为 prototype 的bean声明 <aop:scoped-proxy/> 时,对共享代理的每次方法调用都会导致创建一个新的目标实例,然后调用会被转发到这个新实例上。
此外,使用限定范围的代理并不是以生命周期安全的方式访问较短作用域内bean的唯一方法。你还可以将注入点(即构造函数、setter参数或自动装配的字段)声明为 ObjectFactory<MyTargetBean>,这样就可以在每次需要时通过 getObject() 调用来获取当前实例——而无需保留该实例或单独存储它。
作为一种扩展的用法,你可以声明 ObjectProvider<MyTargetBean>,它提供了几种额外的访问方式,包括 getIfAvailable 和 getIfUnique。
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>
定义代理的那一行。
要创建这样的代理,你需要将一个子元素 <aop:scoped-proxy/> 插入到有作用域的 bean 定义中(参见 选择要创建的代理类型 和 基于 XML Schema 的配置)。
为什么在常见的场景中,作用域为request、session和自定义作用域(custom-scope)的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对象(也就是最初被注入时所使用的那个对象)。
当将生命周期较短的 scoped bean 注入到生命周期较长的 scoped bean 中时(例如,将一个 HTTP Session scope 的协作 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-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>
有关选择基于类或基于接口的代理的更多详细信息,请参阅代理机制。
直接注入请求/会话引用
作为工厂作用域(factory scopes)的替代方案,Spring的WebApplicationContext还支持将HttpServletRequest、HttpServletResponse、HttpSession、WebRequest以及(如果项目中使用了JSF的话)FacesContext和ExternalContext注入到由Spring管理的bean中,这种方式与对其他bean进行常规注入的方式类似,都是通过基于类型的自动注入(type-based autowiring)来实现的。Spring通常会为这些请求和会话对象注入代理(proxies),这样做的好处是,这样的代理也能在单例bean(singleton beans)和可序列化bean(serializable beans)中正常工作,这与工厂作用域bean的代理机制类似。
自定义作用域
bean作用域机制是可扩展的。你可以定义自己的作用域,甚至可以重新定义现有的作用域,尽管后者被视为不良实践,而且你不能覆盖内置的singleton和prototype作用域。
创建自定义作用域
要将您自定义的作用域(scopes)集成到Spring容器中,您需要实现org.springframework.beans.factory.config.Scope接口,本节将对该接口进行说明。关于如何实现自己的作用域的示例,您可以参考Spring框架本身提供的Scope实现,以及Javadoc文档,其中更详细地解释了需要实现的方法。
Scope接口有四种方法,用于从作用域中获取对象、将对象从作用域中移除以及让这些对象被销毁。
例如,会话作用域(session scope)的实现会返回该作用域下的Bean;如果该Bean不存在,该方法会在将其绑定到会话中以供后续引用后,返回该Bean的一个新实例。以下方法则会从底层作用域中返回相应的对象:
- Java
- Kotlin
Object get(String name, ObjectFactory<?> objectFactory)
fun get(name: String, objectFactory: ObjectFactory<*>): Any
例如,会话范围(session scope)的实现会从底层的会话(underlying 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的scope实现。
以下方法用于获取底层范围的对话标识符:
- Java
- Kotlin
String getConversationId()
fun getConversationId(): String
此标识符在每个作用域内都是不同的。对于会话范围(session scope)的实现来说,该标识符可以是会话标识符(session identifier)。
使用自定义作用域
在编写和测试一个或多个自定义的Scope实现之后,你需要让Spring容器知道这些新的作用域(scopes)。以下方法是向Spring容器注册新Scope的核心方法:
- Java
- Kotlin
void registerScope(String scopeName, Scope scope);
fun registerScope(scopeName: String, scope: Scope)
此方法在ConfigurableBeanFactory接口上被声明,而ConfigurableBeanFactory接口可以通过大多数随Spring一起提供的具体ApplicationContext实现中的BeanFactory属性来获取。
registerScope(..)方法的第一个参数是与某个作用域(scope)相关联的唯一名称。在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实现,你不必局限于以编程方式注册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()返回的对象。