Bean 作用域

当你创建一个 bean 定义时,你实际上是为创建该 bean 定义所定义的类的实际实例创建了一份“菜谱”。将 bean 定义看作一份“菜谱”是很重要的,因为这意味着,就像类一样,你可以从一份“菜谱”创建出许多对象实例。

你不仅可以控制要注入到从特定 bean 定义创建的对象中的各种依赖项和配置值,还可以控制从特定 bean 定义创建的对象的“作用域”(scope)。这种方法强大且灵活,因为你可以通过配置来选择创建对象的“作用域”,而不必在 Java 类级别中硬编码对象的“作用域”。可以定义 bean 以部署在多种作用域中的一种。Spring Framework 支持六种作用域,其中四种只有在使用支持 Web 的 ApplicationContext 时才可用。你还可以创建自定义作用域

下表描述了支持的作用域

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

singleton

(默认)将单个 bean 定义限定到每个 Spring IoC 容器中的单个对象实例。

prototype

将单个 bean 定义限定到任意数量的对象实例。

request

将单个 bean 定义限定到单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都拥有基于该单个 bean 定义创建的自己的 bean 实例。仅在支持 Web 的 Spring ApplicationContext 环境中有效。

session

将单个 bean 定义限定到 HTTP Session 的生命周期。仅在支持 Web 的 Spring ApplicationContext 环境中有效。

application

将单个 bean 定义限定到 ServletContext 的生命周期。仅在支持 Web 的 Spring ApplicationContext 环境中有效。

websocket

将单个 bean 定义限定到 WebSocket 的生命周期。仅在支持 Web 的 Spring ApplicationContext 环境中有效。

虽然提供了 thread 作用域,但默认不注册。更多信息,请参阅 SimpleThreadScope 的文档。关于如何注册此作用域或任何其他自定义作用域的说明,请参阅 使用自定义作用域

Singleton 作用域

Spring 容器只管理一个共享的 singleton bean 实例,所有对 ID 或与该 bean 定义匹配的 ID 的 bean 的请求都将返回该特定的 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 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 作用域:

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 作用域

requestsessionapplicationwebsocket 作用域仅在使用支持 Web 的 Spring ApplicationContext 实现(例如 XmlWebApplicationContext)时可用。如果你将这些作用域用于常规的 Spring IoC 容器(例如 ClassPathXmlApplicationContext),则会抛出 IllegalStateException,提示未知的 bean 作用域。

初始 Web 配置

为了支持 requestsessionapplicationwebsocket 级别的 bean 作用域(即 Web 作用域的 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 请求对象绑定到正在处理该请求的线程。这使得 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 之间使用 <aop:scoped-proxy/>,此时引用会通过一个中间的可序列化代理,因此能够在反序列化时重新获取 target singleton bean。

当针对 prototype 作用域的 bean 声明 <aop:scoped-proxy/> 时,对共享代理的每一次方法调用都会导致创建一个新的 target 实例,然后将调用转发给该新实例。

此外,作用域代理并不是以生命周期安全的方式访问来自较短作用域 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 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 还支持将 HttpServletRequestHttpServletResponseHttpSessionWebRequest 以及(如果存在 JSF)FacesContextExternalContext 注入到 Spring 管理的 bean 中,只需通过基于类型的自动装配即可,就像注入其他 bean 的常规注入点一样。Spring 通常为这些请求和会话对象注入代理,这具有在 singleton bean 和可序列化 bean 中也能工作的优点,类似于工厂作用域 bean 的作用域代理。

自定义作用域

bean 的作用域机制是可扩展的。你可以定义自己的作用域,甚至可以重新定义现有的作用域,尽管后者被认为是不良实践,并且你不能覆盖内置的 singletonprototype 作用域。

创建自定义作用域

要将你的自定义作用域集成到 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 容器自身的此类名称示例有 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>
当你在 FactoryBean 实现的 <bean> 声明中放置 <aop:scoped-proxy/> 时,被赋予作用域的是工厂 bean 本身,而不是从 getObject() 返回的对象。