Bean作用域

当您创建一个bean定义时,实际上创建了一个用于创建由该bean定义定义的类的实际实例的配方。bean定义是一个配方的概念很重要,因为这意味着,与类一样,您可以从单个配方创建许多对象实例。

您不仅可以控制要插入到从特定bean定义创建的对象中的各种依赖项和配置值,还可以控制从特定bean定义创建的对象的作用域。这种方法是强大且灵活的,因为您可以通过配置选择要创建的对象的作用域,而不必在Java类级别固定对象的作用域。Bean可以被定义为部署在多个作用域之一。Spring框架支持六种作用域,其中四种仅在使用web-aware ApplicationContext时才可用。您还可以创建自定义作用域。

以下表格描述了支持的作用域:

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

singleton

(默认) 将单个bean定义的作用域限定为每个Spring IoC容器的单个对象实例。

prototype

将单个bean定义的作用域限定为任意数量的对象实例。

request

将单个bean定义的作用域限定为单个HTTP请求的生命周期。也就是说,每个HTTP请求都有一个基于单个bean定义创建的bean实例。仅在web-aware Spring ApplicationContext的上下文中有效。

session

将单个bean定义的作用域限定为HTTP Session的生命周期。仅在web-aware Spring ApplicationContext的上下文中有效。

application

将单个bean定义的作用域限定为ServletContext的生命周期。仅在web-aware Spring ApplicationContext的上下文中有效。

websocket

将单个bean定义的作用域限定为WebSocket的生命周期。仅在web-aware Spring ApplicationContext的上下文中有效。

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

单例作用域

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

换句话说,当您定义一个bean定义并将其作用域设置为单例时,Spring IoC容器会创建该bean定义所定义的对象的一个实例。这个单个实例存储在这些单例bean的缓存中,所有后续对该命名bean的请求和引用都会返回缓存的对象。以下图片展示了单例作用域的工作原理:

singleton

Spring的单例bean概念与《设计模式》一书中定义的单例模式有所不同。《设计模式》中的单例模式将对象的作用域硬编码为每个ClassLoader只创建一个特定类的实例。Spring的单例作用域最好描述为每个容器和每个bean。这意味着,如果您在单个Spring容器中为特定类定义一个bean,Spring容器将创建该bean定义的类的一个且仅一个实例。单例作用域是Spring中的默认作用域。要在XML中将一个bean定义为单例,您可以像以下示例中所示定义一个bean:

<bean id="accountService" class="com.something.DefaultAccountService"/>

<!-- 以下是等效的,尽管多余(单例作用域是默认的) -->
<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依赖关系的单例Beans

当您使用具有原型bean依赖关系的单例作用域bean时,请注意依赖关系在实例化时解析。因此,如果您将一个原型作用域bean注入到一个单例作用域bean中,将实例化一个新的原型bean,然后将其依赖注入到单例bean中。原型实例是唯一提供给单例作用域bean的实例。

然而,假设您希望单例作用域bean在运行时重复获取原型作用域bean的新实例。您不能将原型作用域bean注入到单例bean中,因为该注入仅在Spring容器实例化单例bean并解析和注入其依赖项时发生一次。如果您需要在运行时多次获取原型bean的新实例,请参阅方法注入

请求、会话、应用程序和WebSocket作用域

requestsessionapplicationwebsocket作用域仅在您使用支持Web的Spring ApplicationContext实现(例如XmlWebApplicationContext)时才可用。如果您在常规的Spring IoC容器中使用这些作用域,比如ClassPathXmlApplicationContext,将抛出一个关于未知bean作用域的IllegalStateException异常。

初始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 over WebSocket,请参阅WebSocket作用域了解更多详情。

作为依赖项的作用域Bean

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

您还可以在作用域为singleton的bean之间使用<aop:scoped-proxy/>,然后引用将通过一个可序列化的中间代理传递到目标singleton bean上,因此能够在反序列化时重新获取目标singleton 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">

	<!-- 作为代理公开的HTTP会话作用域bean -->
	<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
		<!-- 指示容器代理周围的bean -->
		<aop:scoped-proxy/> (1)
	</bean>

	<!-- 通过代理注入到上述bean的单例作用域bean -->
	<bean id="userService" class="com.something.SimpleUserService">
		<!-- 对代理的userPreferences bean的引用 -->
		<property name="userPreferences" ref="userPreferences"/>
	</bean>
</beans>
1 定义代理的行。

要创建这样的代理,您需要在作用域bean定义中插入一个子<aop:scoped-proxy/>元素(请参见选择要创建的代理类型基于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对象。

因此,当将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代理不会拦截私有方法。尝试在此类代理上调用私有方法将不会委托给实际的作用域目标对象。

或者,您可以配置Spring容器为这些作用域bean创建标准的基于JDK接口的代理,方法是将<aop:scoped-proxy/>元素的proxy-target-class属性的值指定为false。使用基于JDK接口的代理意味着您不需要在应用程序类路径中添加额外的库来影响此类代理。但是,这也意味着作用域bean的类必须实现至少一个接口,并且所有注入作用域bean的协作者必须通过其中一个接口引用该bean。以下示例显示了基于接口的代理:

<!-- DefaultUserPreferences实现UserPreferences接口 -->
<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的新实例,之后将其绑定到会话以供将来引用)。以下方法从底层作用域返回对象:

  • 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实现,然后按照下一个示例中所示进行注册。

下一个示例使用了Spring中包含的SimpleThreadScope,但默认情况下未注册。对于您自己的自定义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>
当您将<aop:scoped-proxy/>放在<bean>声明中用于FactoryBean实现时,被作用域化的是工厂bean本身,而不是从getObject()返回的对象。