Bean 作用域

当您创建 Bean 定义时,您会创建一个用于创建 Bean 定义所定义的类的实际实例的配方。Bean 定义是配方的理念很重要,因为它意味着,与类一样,您可以从单个配方创建多个对象实例。

您不仅可以控制要插入从特定 Bean 定义创建的对象的各种依赖项和配置值,还可以控制从特定 Bean 定义创建的对象的范围。这种方法功能强大且灵活,因为您可以通过配置选择创建的对象的范围,而不必在 Java 类级别将对象的范围烘焙进去。Bean 可以定义为部署在多个范围之一中。Spring 框架支持六个范围,其中四个仅在您使用 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的上下文中有效。

线程范围可用,但默认情况下未注册。有关更多信息,请参阅SimpleThreadScope的文档。有关如何注册此范围或任何其他自定义范围的说明,请参阅使用自定义范围

单例范围

仅管理一个共享的单例 Bean 实例,并且对具有与该 Bean 定义匹配的 ID 或 ID 的 Bean 的所有请求都会导致 Spring 容器返回该一个特定 Bean 实例。

换句话说,当您定义 Bean 定义并将其范围限定为单例时,Spring IoC 容器会准确创建该 Bean 定义所定义的对象的一个实例。此单个实例存储在这些单例 Bean 的缓存中,并且随后对该命名 Bean 的所有请求和引用都返回缓存的对象。下图显示了单例范围的工作原理

singleton

Spring 的单例 Bean 概念不同于 Gang of Four (GoF) 模式书籍中定义的单例模式。GoF 单例对对象的范围进行硬编码,以便每个类加载器仅创建特定类的一个实例。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 原型范围

prototype

(数据访问对象 (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 范围

只有当您使用支持 Web 的 Spring ApplicationContext 实现(例如 XmlWebApplicationContext)时,才能使用 requestsessionapplicationwebsocket 范围。如果您在常规 Spring IoC 容器(例如 ClassPathXmlWebApplicationContext)中使用这些范围,则会抛出 IllegalStateException,并提示存在未知 Bean 范围。

初始 Web 配置

为了支持在 requestsessionapplicationwebsocket 级别(Web 范围的 Bean)对 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 请求对象绑定到正在服务该请求的 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 会话的生命周期相关联,并应用于通过 WebSocket 的 STOMP 应用程序,请参阅 WebSocket 范围以了解更多详细信息。

范围限定的 Bean 作为依赖项

Spring IoC 容器不仅管理对象的实例化(Bean),还管理协作者(或依赖项)的连接。如果您想将(例如)HTTP 请求范围的 Bean 注入到另一个生命周期更长的 Bean 中,可以选择注入 AOP 代理来代替范围限定的 Bean。也就是说,您需要注入一个代理对象,该对象公开与范围限定对象相同的公共接口,但还可以从相关范围(例如 HTTP 请求)检索实际的目标对象,并将方法调用委托给实际对象。

您也可以在范围限定为 singleton 的 Bean 之间使用 <aop:scoped-proxy/>,然后通过一个可序列化的中间代理进行引用,因此能够在反序列化时重新获取目标单例 Bean。

当针对范围限定为 prototype 的 Bean 声明 <aop:scoped-proxy/> 时,对共享代理的每次方法调用都会导致创建一个新的目标实例,然后将调用转发到该实例。

此外,范围限定的代理不是以生命周期安全的方式访问较短范围的 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 架构的配置)。

为什么在常见情况下,范围限定在 requestsession 和自定义范围级别的 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 SessionuserPreferences 对象。因此,容器创建了一个对象,该对象公开了与 UserPreferences 类完全相同的公共接口(理想情况下是 UserPreferences 实例的对象),它可以从范围限定机制(HTTP 请求、Session 等)中获取真实的 UserPreferences 对象。容器将此代理对象注入到 userManager Bean 中,userManager Bean 并不知道此 UserPreferences 引用是一个代理。在此示例中,当 UserManager 实例调用依赖项注入的 UserPreferences 对象上的方法时,它实际上是在调用代理上的方法。然后,代理从(在本例中为)HTTP Session 中获取真实的 UserPreferences 对象,并将方法调用委托给检索到的真实的 UserPreferences 对象。

因此,当将 requestsession 范围的 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>

有关选择基于类的代理或基于接口的代理的更多详细信息,请参阅 代理机制

直接注入请求/会话引用

作为工厂范围的替代方案,Spring WebApplicationContext 还支持将 HttpServletRequestHttpServletResponseHttpSessionWebRequest 和(如果存在 JSF)FacesContextExternalContext 注入到 Spring 管理的 Bean 中,只需通过基于类型的自动装配即可,与其他 Bean 的常规注入点并排。Spring 通常会为这些请求和会话对象注入代理,这具有在单例 Bean 和可序列化 Bean 中也起作用的优点,类似于工厂范围的 Bean 的范围限定代理。

自定义范围

Bean 范围限定机制是可扩展的。您可以定义自己的范围,甚至重新定义现有范围,尽管后者被认为是不好的做法,并且您不能覆盖内置的 singletonprototype 范围。

创建自定义范围

要将您的自定义范围集成到 Spring 容器中,您需要实现 org.springframework.beans.factory.config.Scope 接口,本节将对此进行描述。有关如何实现自己的范围的想法,请参阅随 Spring 框架本身提供的 Scope 实现和 Scope javadoc,其中详细解释了您需要实现的方法。

Scope 接口有四种方法来从作用域获取对象,从作用域移除对象,以及让它们被销毁。

例如,会话作用域实现返回会话作用域的 Bean(如果它不存在,则该方法在将 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 容器本身中此类名称的示例包括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()返回的对象。