对于最新的稳定版本,请使用 Spring Framework 7.0.6!spring-doc.cadn.net.cn

Bean 作用域

当你创建一个Bean定义时,你实际上是在创建一个用于生成由该Bean定义所定义的类的实例的“配方”。一个Bean定义是一个“配方”的概念非常重要,因为这意味着与类类似,你可以从一个配方中创建出多个对象实例。spring-doc.cadn.net.cn

您可以控制不仅包括要插入从特定bean定义创建的对象的各种依赖项和配置值,还可以控制从特定bean定义创建的对象的作用域。这种方法非常强大且灵活,因为您可以选择通过配置创建的对象的作用域,而不是必须在Java类级别上固定对象的作用域。可以定义beans以在多种作用域中部署。 Spring框架支持六种作用域,其中四种只有在使用web感知的<code>0</code>时才可用。您还可以创建 自定义作用域。spring-doc.cadn.net.cn

下表描述了支持的作用域:spring-doc.cadn.net.cn

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

单例spring-doc.cadn.net.cn

(默认)将单个bean定义作用于每个Spring IoC容器的单个对象实例。spring-doc.cadn.net.cn

原型spring-doc.cadn.net.cn

将单个 bean 定义作用于任意数量的对象实例。spring-doc.cadn.net.cn

请求spring-doc.cadn.net.cn

将单个Bean定义的作用域限定为单个HTTP请求的生命周期。也就是说, 每个HTTP请求都会根据一个单一的Bean定义创建一个Bean的实例。 仅在具有Web感知能力的Spring ApplicationContext上下文中有效。spring-doc.cadn.net.cn

会话spring-doc.cadn.net.cn

将单个Bean定义的作用域限定为HTTP Session的生命周期。仅在具有Web感知能力的Spring ApplicationContext上下文中有效。spring-doc.cadn.net.cn

应用spring-doc.cadn.net.cn

将单个bean定义的作用域限定为ServletContext的生命周期。仅在具备web支持的Spring ApplicationContext上下文中有效。spring-doc.cadn.net.cn

WebSocketspring-doc.cadn.net.cn

将单个bean定义的作用域限定为WebSocket的生命周期。仅在具备web支持的Spring ApplicationContext上下文中有效。spring-doc.cadn.net.cn

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

单例作用域

只有一个共享的单例 bean 实例被管理,所有请求 ID 或 IDs 与该 bean 定义匹配的 bean 的请求,都会导致 Spring 容器返回该特定的 bean 实例。spring-doc.cadn.net.cn

换句话说,当你定义一个 Bean 定义,并且它的作用域设置为 singleton 时,Spring IoC 容器会为该 Bean 定义所定义的对象创建一个实例。这个单一实例被存储在一个 singleton Bean 的缓存中,所有后续对该命名 Bean 的请求和引用都会返回缓存中的对象。下图展示了 singleton 作用域的工作方式:spring-doc.cadn.net.cn

singleton

Spring的单例bean概念与Gang of Four(GoF)模式书中定义的单例模式不同。GoF单例硬编码了对象的作用域,使得每个类加载器只创建一个特定类的实例。Spring单例的作用域最好描述为按容器和按bean来确定。这意味着,如果你在单个Spring容器中为某个类定义了一个bean,Spring容器将只创建该bean定义所定义的类的一个实例。单例作用域是Spring的默认作用域。要在XML中将一个bean定义为单例,可以按照以下示例定义一个bean:spring-doc.cadn.net.cn

<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-doc.cadn.net.cn

下图展示了 Spring 原型作用域:spring-doc.cadn.net.cn

prototype

数据访问对象(DAO)通常不会被配置为原型,因为典型的DAO不保存任何会话状态。对我们来说,复用单例模式的核心更容易。spring-doc.cadn.net.cn

以下示例在 XML 中将 bean 定义为原型:spring-doc.cadn.net.cn

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

与其他作用域不同,Spring 不会管理原型(prototype)Bean 的完整生命周期。容器会实例化、配置并组装原型对象,然后将其交给客户端,而不再记录该原型实例。因此,尽管无论作用域如何都会调用初始化生命周期回调方法,但在原型作用域的情况下,配置的销毁生命周期回调方法不会被调用。客户端代码必须清理原型作用域的对象,并释放原型 Bean 所持有的昂贵资源。要让 Spring 容器释放原型作用域 Bean 持有的资源,请尝试使用一个自定义的 bean 后处理器,该处理器保留需要清理的 Bean 的引用。spring-doc.cadn.net.cn

在某些方面,Spring容器对于原型作用域的bean的作用是Java new运算符的替代。在此之后的所有生命周期管理都必须由客户端处理。(有关Spring容器中bean的生命周期的详细信息,请参阅生命周期回调。)spring-doc.cadn.net.cn

具有原型Bean依赖项的单例Bean

当您使用依赖于原型作用域Bean的单例作用域Bean时,请注意依赖关系是在实例化时解析的。因此,如果您将一个原型作用域的Bean注入到一个单例作用域的Bean中,会实例化一个新的原型Bean,然后将其依赖注入到单例Bean中。该原型实例是唯一提供给单例作用域Bean的实例。spring-doc.cadn.net.cn

然而,假设你希望单例作用域的bean在运行时反复获取原型作用域bean的新实例。你不能将原型作用域的bean依赖注入到你的单例bean中,因为这种注入只会在Spring容器实例化单例bean并解析和注入其依赖项时发生一次。如果你需要在运行时多次获取原型bean的新实例,请参阅方法注入spring-doc.cadn.net.cn

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

requestsessionapplicationwebsocket 范围仅在您使用基于网络的 Spring ApplicationContext 实现时可用(例如 XmlWebApplicationContext)。如果您将这些范围与常规 Spring IoC 容器一起使用,例如 ClassPathXmlApplicationContext,则会抛出一个指出未知 bean 范围的 IllegalStateExceptionspring-doc.cadn.net.cn

初始 Web 配置

为了支持在 requestsessionapplicationwebsocket 级别(Web 范围的 Bean)中对 Bean 进行作用域划分,在定义您的 Bean 之前需要进行一些小的初始配置。(此初始设置对于标准作用域:singletonprototype 不是必需的。)spring-doc.cadn.net.cn

你完成此初始设置的方式取决于你的特定Servlet环境。spring-doc.cadn.net.cn

如果您在Spring Web MVC中访问作用域Bean,实际上是在由Spring DispatcherServlet处理的请求中,不需要进行特殊设置。 DispatcherServlet已经公开了所有相关状态。spring-doc.cadn.net.cn

如果您使用的是 Servlet 网络容器,且请求在 Spring 的 DispatcherServlet 之外处理(例如使用 JSF 时),则需要注册 org.springframework.web.context.request.RequestContextListener ServletRequestListener。 这可以通过使用 WebApplicationInitializer 接口来编程完成。 或者,将以下声明添加到您的 Web 应用程序的 web.xml 文件中:spring-doc.cadn.net.cn

<web-app>
	...
	<listener>
		<listener-class>
			org.springframework.web.context.request.RequestContextListener
		</listener-class>
	</listener>
	...
</web-app>

或者,如果您遇到监听器设置的问题,可以考虑使用 Spring 的 RequestContextFilter。过滤器映射取决于周围的 Web 应用程序配置,因此您需要根据需要进行更改。下面的列表显示了 Web 应用程序的过滤器部分:spring-doc.cadn.net.cn

<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 在调用链的更深处可用。spring-doc.cadn.net.cn

请求范围

考虑以下Bean定义的XML配置:spring-doc.cadn.net.cn

<bean id="loginAction" class="com.something.LoginAction" scope="request"/>

Spring容器通过为每个HTTP请求使用LoginAction bean定义来创建loginAction bean的新实例。也就是说,loginAction bean在HTTP请求级别上具有作用域。您可以随意更改由此创建的实例的内部状态,因为其他从同一loginAction bean定义创建的实例不会看到这些状态变化。它们仅适用于单个请求。当请求完成处理后,作用域为请求的bean将被丢弃。spring-doc.cadn.net.cn

在使用注解驱动的组件或Java配置时,可以使用@RequestScope注解将组件分配到request作用域。下面的例子展示了如何操作:spring-doc.cadn.net.cn

@RequestScope
@Component
public class LoginAction {
	// ...
}
@RequestScope
@Component
class LoginAction {
	// ...
}

会话范围

考虑以下Bean定义的XML配置:spring-doc.cadn.net.cn

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

Spring 容器通过使用单个 HTTP UserPreferencesuserPreferences bean 定义来创建 Session bean 的新实例。换句话说,userPreferences bean 在 HTTP Session 层面实际上是作用域限定的。与请求作用域的 bean 一样,您可以随意更改由该 Session bean 定义创建的实例的内部状态,知道其他也使用从同一 userPreferences bean 定义创建的实例的 HTTP Session 实例不会看到这些状态更改,因为它们是特定于单个 HTTP Session 的。当 HTTP Session 最终被丢弃时,作用域限定于该特定 HTTP 10 的 bean 也会被丢弃。spring-doc.cadn.net.cn

在使用注解驱动的组件或Java配置时,可以使用 @SessionScope 注解将组件分配到 session 范围。spring-doc.cadn.net.cn

@SessionScope
@Component
public class UserPreferences {
	// ...
}
@SessionScope
@Component
class UserPreferences {
	// ...
}

应用范围

考虑以下Bean定义的XML配置:spring-doc.cadn.net.cn

<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>

Spring容器通过使用AppPreferences bean定义为整个Web应用程序创建一个新的appPreferences bean实例。也就是说,appPreferences bean的作用域在ServletContext级别,并作为常规的ServletContext属性存储。这在某种程度上类似于Spring单例bean,但在两个重要方面有所不同:它是每个ServletContext的单例,而不是每个SpringApplicationContext的单例(在任何给定的Web应用程序中可能有多个),并且它实际上是公开的,因此作为ServletContext属性可见。spring-doc.cadn.net.cn

在使用注解驱动的组件或Java配置时,可以使用 @ApplicationScope 注解将组件分配到 application 范围。下面的示例显示了如何操作:spring-doc.cadn.net.cn

@ApplicationScope
@Component
public class AppPreferences {
	// ...
}
@ApplicationScope
@Component
class AppPreferences {
	// ...
}

WebSocket 作用域

WebSocket 作用域与 WebSocket 会话的生命周期相关,并适用于 STOMP over WebSocket 应用程序,有关详细信息请参见 WebSocket 作用域spring-doc.cadn.net.cn

作用域Bean作为依赖项

Spring IoC 容器不仅管理你的对象(Bean)的实例化, 还负责协作对象(或依赖项)的连接。如果你希望将一个 HTTP 请求作用域的 Bean 注入到另一个作用域更长的 Bean 中,你可以选择注入一个 AOP 代理来代替该作用域的 Bean。也就是说,你需要注入一个代理对象,该对象暴露与作用域对象相同的公共接口,但也可以从相关作用域(如 HTTP 请求)中检索真正的目标对象,并将方法调用委托给真实对象。spring-doc.cadn.net.cn

您还可以在作用域为 singleton 的 beans 之间使用 <aop:scoped-proxy/>, 然后通过一个可序列化的中间代理进行引用, 因此可以在反序列化时重新获取目标单例 bean。spring-doc.cadn.net.cn

在将 <aop:scoped-proxy/> 声明为作用域为 prototype 的 bean 时,对共享代理的每个方法调用都会导致创建一个新的目标实例,并将调用转发到该实例。spring-doc.cadn.net.cn

此外,作用域代理并不是以生命周期安全的方式从较短作用域中访问Bean的唯一方法。您还可以将您的注入点(即构造函数或设置器参数或自动连线字段)声明为ObjectFactory<MyTargetBean>,从而在每次需要时通过getObject()调用检索当前实例——而无需保留实例或单独存储它。spring-doc.cadn.net.cn

作为扩展变体,您可以声明 ObjectProvider<MyTargetBean>,这将提供几种额外的访问方式,包括 getIfAvailablegetIfUniquespring-doc.cadn.net.cn

此JSR-330变体称为Provider,并用于带有Provider<MyTargetBean>声明的场景,每次检索尝试都需要相应的get()调用。 有关JSR-330的更多信息,请参见这里spring-doc.cadn.net.cn

以下示例中的配置只有一行,但理解其背后的“为什么”以及“如何做”是很重要的:spring-doc.cadn.net.cn

<?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 的配置)。spring-doc.cadn.net.cn

为什么在requestsession和自定义作用域级别的Bean定义中,通常需要<aop:scoped-proxy/>元素? 考虑以下单例Bean定义,并与上述作用域所需的定义进行对比(请注意,以下userPreferencesBean定义目前是不完整的):spring-doc.cadn.net.cn

<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对象(即它最初被注入的那个对象)。spring-doc.cadn.net.cn

这不是在将生命周期较短的作用域Bean注入到生命周期较长的作用域Bean时所期望的行为(例如,将HTTP Session作用域的协作Bean作为依赖项注入到单例Bean中)。相反,你需要一个userManager对象,并且在HTTP Session的生命周期内,你需要一个特定于HTTP SessionuserPreferences对象。因此,容器会创建一个暴露与UserPreferences类完全相同公共接口的对象(最好是UserPreferences实例),该对象可以从作用域机制(HTTP请求,Session等)中获取真正的UserPreferences对象。容器将这个代理对象注入到userManager Bean中,而该Bean并不知道这个UserPreferences引用是一个代理。在这个例子中,当UserManager实例调用依赖注入的UserPreferences对象上的方法时,实际上是在调用代理上的方法。然后代理会从(在此情况下)HTTP Session中获取真正的UserPreferences对象,并将方法调用委托给获取到的真正的UserPreferences对象。spring-doc.cadn.net.cn

因此,当将request-session-scoped beans 注入到协作对象中时,你需要以下(正确且完整的)配置,如下例所示:spring-doc.cadn.net.cn

<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的类代理。spring-doc.cadn.net.cn

CGLIB 代理不会拦截私有方法。尝试在这样的代理上调用私有方法不会将调用委托给实际的作用域目标对象。spring-doc.cadn.net.cn

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

<!-- 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-doc.cadn.net.cn

自定义范围

Bean的作用域机制是可扩展的。您可以定义自己的作用域,甚至可以重新定义现有的作用域,尽管后一种做法被认为是不好的实践,您不能覆盖内置的 singletonprototype 作用域。spring-doc.cadn.net.cn

创建自定义作用域

要将自定义作用域集成到Spring容器中,您需要实现 org.springframework.beans.factory.config.Scope接口,本 spring-doc.cadn.net.cn

中对此进行了描述。有关如何实现自己的作用域的思路,请参见Spring框架本身提供的 Scope 实现以及 Scope javadoc, 其中更详细地解释了您需要实现的方法。

spring-doc.cadn.net.cn

Scope 接口有四个方法可以从作用域中获取对象、从作用域中移除它们,并让它们被销毁。spring-doc.cadn.net.cn

会话范围的实现,例如,返回会话作用域的 bean(如果它不存在,该方法将返回该 bean 的新实例,并将其绑定到会话中以供以后引用)。以下方法从底层作用域中返回对象:spring-doc.cadn.net.cn

Object get(String name, ObjectFactory<?> objectFactory)
fun get(name: String, objectFactory: ObjectFactory<*>): Any

会话范围的实现,例如,会从底层会话中删除会话范围的bean。应该返回该对象,但如果找不到指定名称的对象,可以返回null。以下方法会从底层作用域中删除该对象:spring-doc.cadn.net.cn

Object remove(String name)
fun remove(name: String): Any

以下方法注册了一个回调,当作用域被销毁时或作用域中的指定对象被销毁时,该作用域应调用此回调:spring-doc.cadn.net.cn

void registerDestructionCallback(String name, Runnable destructionCallback)
fun registerDestructionCallback(name: String, destructionCallback: Runnable)

查看 javadoc 或 Spring 范围实现以了解有关销毁回调的更多信息。spring-doc.cadn.net.cn

以下方法获取底层作用域的对话标识符:spring-doc.cadn.net.cn

String getConversationId()
fun getConversationId(): String

这个标识符在每个作用域中都是不同的。对于会话作用域的实现,此标识符可以是会话标识符。spring-doc.cadn.net.cn

使用自定义作用域

在您编写并测试了一个或多个自定义 Scope 实现后,需要让 Spring 容器了解您的新作用域。以下方法是向 Spring 容器注册新 Scope 的核心方法:spring-doc.cadn.net.cn

void registerScope(String scopeName, Scope scope);
fun registerScope(scopeName: String, scope: Scope)

此方法在 ConfigurableBeanFactory 接口中声明,该接口可通过大多数带有 Spring 的具体 ApplicationContext 实现的 BeanFactory 属性获得。spring-doc.cadn.net.cn

方法 registerScope(..) 的第一个参数是与作用域相关联的唯一名称。Spring 容器本身中此类名称的示例包括 singletonprototype。方法 registerScope(..) 的第二个参数是你希望注册并使用的自定义 Scope 实现的实际实例。spring-doc.cadn.net.cn

假设您编写了自己的 Scope 实现,然后如下面的示例所示进行注册。spring-doc.cadn.net.cn

下一个示例使用了 SimpleThreadScope,它随 Spring 一起提供,但默认情况下未注册。对于您自己的自定义 Scope 实现,操作步骤相同。
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
val threadScope = SimpleThreadScope()
beanFactory.registerScope("thread", threadScope)

然后,您可以创建符合您自定义作用域规则的bean定义, Scope,如下所示:spring-doc.cadn.net.cn

<bean id="..." class="..." scope="thread">

使用自定义的 Scope 实现,您不限于通过编程方式注册作用域。您还可以通过使用 Scope 类,以声明的方式进行 CustomScopeConfigurer 注册,如下例所示:spring-doc.cadn.net.cn

<?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() 返回的对象。