跳至主要內容

Bean作用域(Scope)

会敲代码的程序猿原创SpringSpring Framework大约 16 分钟

Bean作用域(Scope)

当你创建一个Bean定义时,实际上是在创建Bean定义所定义类的实际实例的配方。 将Bean定义视为“配方”的概念非常重要,因为它意味着,就像一个类一样,你可以从一个单一的“配方”中创建多个对象实例。

你不仅可以控制Bean定义中的各种依赖项和配置值,还可以控制由Bean定义创建的对象的作用域(scope)。 这种方法是强大且灵活的,因为你可以通过配置选择创建的对象的作用域,而不必在Java类级别上固定对象的作用域。 Bean定义可以是多种作用域之一。Spring框架支持六种作用域,其中四种仅在使用Web感知(aware)的ApplicationContext时才可用。 你还可以创建自定义作用域open in new window

Bean作用域(Scope)描述
singleton(默认) 在整个应用程序中只创建一个Bean实例
prototype每次请求时,创建一个新的Bean实例
requestWeb程序中,为每个HTTP请求创建一个Bean实例
sessionWeb程序中,为每个HTTP会话创建一个Bean实例
applicationWeb程序中,为每个ServletContext创建一个Bean实例
websocketWeb程序中,为每个WebSocket连接创建一个Bean实例

线程作用域(Thread Scope)在Spring框架中是可用的,但默认情况下并没有注册。参阅 SimpleThreadScopeopen in new window。 关于如何注册此Scope或任何其他自定义Scope的说明,参阅 自定义Scopeopen in new window

单例作用域(singleton)

单例作用域(singleton scope)是Spring框架中Bean定义的默认作用域。 当你将一个Bean定义为单例作用域时,对所有具有匹配ID或名称的Bean的调用都会返回这个特定的Bean实例。

下图说明了单例作用域:

singleton
singleton

Spring的单例Bean概念与《设计模式》GoF(四人帮)书中定义的单例模式有所不同。

  • GoF单例模式通过硬编码对象的作用域,确保每个类加载器(ClassLoader)下,仅有一个特定类的实例被创建
  • Spring单例的作用域最好被描述为每个容器(per-container)和每个bean(per-bean)

单例作用域是Spring中的默认作用域。要在XML中将一个Bean定义为单例,参考按照以下示例:

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

<!-- 以下是等效的冗余写法(因为单例作用域是默认的) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

原型作用域(prototype)

原型作用域(prototype scope)的Bean部署,意味着每次请求该特定Bean时都会创建一个新的Bean实例。 也就是说,当一个Bean被注入到另一个Bean中,或者通过容器上的getBean()方法调用请求它,每次都会产生一个新的实例。 作为一项规则,将原型(prototype)作用域用于所有有状态的Bean,将单例(singleton)作用域用于无状态的Bean。

下图说明了原型作用域:

prototype
prototype

(注意⚠️:以上图片中的数据访问对象(DAO)通常不配置为原型作用域,因为典型的DAO不持有任何会话状态。)

以下示例展示了如何在XML中将一个Bean定义为原型作用域:

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

与其他作用域相比,Spring并不管理原型(prototype)Bean的完整生命周期。 容器实例化、配置并组装原型对象,然后将其交给客户端,之后就不会对那个原型实例保持任何记录。 因此,尽管初始化生命周期回调方法(如@PostConstruct)会在所有对象上调用,而不考虑作用域, 但在原型作用域的情况下,配置的销毁生命周期回调方法(如@PreDestroy)则不会被调用。 客户端代码必须清理原型作用域的对象,并释放原型Bean所持有的昂贵资源。 要让Spring容器释放原型作用域Bean所持有的资源,可以尝试使用一个自定义的Bean后置处理器open in new window ,该后置处理器持有需要清理的Bean的引用。

在某些方面,Spring容器对于原型(prototype)作用域Bean的角色类似于Java中的new运算符。 但是,一旦Spring容器创建并交付原型Bean给客户端,所有生命周期管理的工作都需要由客户端自行处理。 有关Spring容器中Bean的生命周期的详细信息,参阅 生命周期回调open in new window

单例Bean与原型Bean依赖

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

然而,假设你希望单例作用域的Bean在运行时重复获取原型作用域的Bean的新实例。 你不能将一个原型作用域的Bean注入到你的单例Bean中,因为这种注入只会在Spring容器实例化单例Bean并解析并注入其依赖时发生一次。 如果你需要在运行时多次获取原型Bean的新实例,参阅 方法注入(Method Injection)open in new window

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

requestsessionapplicationwebSocket作用域只有在使用Web感知(aware)的Spring应用程序上下文实现, 如XmlWebApplicationContext时才可用。 如果你在常规的Spring IoC容器中使用这些作用域,比如ClassPathXmlApplicationContext, 将会抛出一个IllegalStateException异常,提示未知的Bean作用域。

初始Web配置

对于Web作用域的Bean,即requestsessionapplicationwebsocket的Bean,需要进行特定的作用域范围设置, 初始设置取决于你的特定Servlet环境。 对于标准作用域,如singletonprototype则不需要进行这些初始设置。

如果你在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>

如果你在设置监听器(listener)时遇到问题,可以考虑使用Spring的RequestContextFilter。 过滤器(filter)的映射取决于周围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)上。 这使得请求范围(request-scoped)和会话范围(session-scoped)的Bean在整个调用链下游可用。

请求作用域(request)

以下XML示例中Bean的作用域是HTTP请求(request)级别的:

<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
  • 对于每个HTTP请求,Spring容器会创建一个新的LoginAction实例
  • 每个实例独立,状态改变不会影响其他实例
  • 请求结束后,相关实例被销毁

可以使用@RequestScope注解可将组件限定在请求作用域内:

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

会话作用域(session)

以下XML示例中Bean的作用域是HTTP会话(Session)级别的:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
  • 对于单个HTTP会话(Session),Spring容器会创建一个新的UserPreferences实例
  • 允许会话内状态更改,但不会影响其他会话
  • 当HTTP会话(Session)结束时,相关联的Bean实例也会被销毁

可以使用@SessionScope注解将组件限定在会话作用域内:

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

应用程序作用域(application)

以下XML示例中Bean的作用域是ServletContext级别的:

<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
  • 对于整个Web应用程序,Spring容器仅会创建一个AppPreferences实例,存储在ServletContext属性中
  • 这类似于Spring的单例Bean,但在两个重要方面有所不同:
    1. 它是每个ServletContext的单例,而不是每个Spring ApplicationContext (在任何给定的Web应用程序中可能有多个ApplicationContext
    2. 它实际上是作为ServletContext属性暴露和可见的

可以使用@ApplicationScope注解将组件限定在应用程序作用域内:

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

WebSocket作用域

WebSocket作用域与WebSocket会话的生命周期相关联,适用于基于WebSocket的STOMP应用程序, 详情参阅:WebSocket作用域open in new window

Bean Scope作为依赖项

Spring IoC容器不仅管理对象(Bean)的实例化,还管理协作对象(或依赖项)的注入。 当需要将生命周期较短的Bean(HTTP请求作用域的Bean)注入到生命周期较长的Bean中,可以选择注入一个AOP代理对象。 换句话说,你需要注入一个代理对象,具有与被代理Bean相同的接口,能够从相关作用域获取实际的Bean实例,并代理其方法调用。

你还可以在定义singleton作用域的Bean之间使用 <aop:scoped-proxy/>, 这样引用就会通过一个可序列化的中间代理进行,因此能够在反序列化时重新获取目标singleton Bean。

当对prototype作用域的Bean声明<aop:scoped-proxy/>时,对共享代理的每个方法调用都会导致创建一个新的目标实例,并将调用转发到新创建的实例上。

此外,作用域代理并不是以生命周期安全的方式从较短作用域中访问Bean的唯一方法。 你还可以将注入点(即构造函数或setter参数或autowired字段)声明为ObjectFactory<MyTargetBean>, 允许在每次需要时通过调用getObject()来获取当前实例,而无需持有实例或将其分开存储。

作为一个扩展变体,你还可以声明ObjectProvider<MyTargetBean>,它提供了几个额外的访问变体,包括getIfAvailablegetIfUnique

JSR-330的变体被称为Provider,使用Provider<MyTargetBean>声明,并且每次检索尝试时都需要对应的get()调用。有关JSR-330的更多细节, 请参阅此处open in new window

以下示例中的配置只有一行,但理解其“为什么(why)”以及“如何(how)”背后的原因同样重要:

<?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 Session作用域的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>

要创建userPreferences代理,需要在作用域Bean定义中插入一个子元素<aop:scoped-proxy/> (参阅选择要创建的代理类型open in new window基于XML模式的配置open in new window)。

为什么在requestsession和自定义作用域层次上的Bean定义需要使用<aop:scoped-proxy/>元素?

考虑以下单例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会话作用域的Bean(userPreferences)的引用。 这里关键点是

  • 单例Bean(userManager)它在容器中只被实例化一次,并且它的依赖项userPreferences Bean也只被注入一次
  • 这意味着userManager Bean始终操作同一个的userPreferences对象(即最初注入时的对象),这不是期望的行为

问题描述:单例与会话作用域的交互

当把一个生命周期较短的作用域Bean注入到一个生命周期较长的作用域Bean时,这不是你想要的行为 (例如,在单例Bean中注入一个HTTP Session作用域的协作Bean作为依赖项)。 相反,你需要一个单例的userManager对象,而且,在HTTP Session的生命周期内,你需要一个特定于HTTP Session的userPreferences对象。

解决方案:使用代理对象

因此,容器会创建一个代理对象,具有与被代理Bean相同的接口(最好是一个UserPreferences实例),能够从相关作用域获取实际的Bean实例,并代理其方法调用。 容器将这个代理对象注入到userManager Bean中,而这个userManager Bean并不知道这个UserPreferences引用是一个代理。 在这个例子中,当UserManager实例调用依赖注入的UserPreferences对象上的方法时,实际上是在调用代理上的方法。 然后,代理从HTTP Session中获取真实的UserPreferences对象,并将方法调用委托给真实的UserPreferences对象。

以下是将请求作用域和会话作用域的 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代理只拦截public方法的调用! 不要在这样的代理上调用非public的方法。它们不会被委托给实际的作用域目标对象。

另外,你也可以通过在<aop:scoped-proxy/>元素的proxy-target-class属性中指定false的方式, 配置Spring容器为这些作用域Bean创建基于JDK接口的标准代理。 使用基于JDK接口的代理,意味着你的应用程序 classpath 中不需要额外的库来影响这种代理。 然而,这也意味着作用域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>

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

直接注入request/session引用

作为工厂作用域的替代方案,Spring WebApplicationContext还支持将 HttpServletRequestHttpServletResponseHttpSessionWebRequest 和(如果存在 JSF)FacesContextExternalContext直接注入到Spring管理的Bean中, 只需通过基于类型的自动装配即可,与普通Bean的其他注入点一起。 Spring 通常为这些请求和会话对象注入代理,这样做的好处是可以在单例Bean和可序列化Bean中正常工作,类似于工厂作用域Bean的作用域代理。

自定义作用域

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

创建自定义 Scope

要将自定义作用域集成到Spring容器中,你需要实现org.springframework.beans.factory.config.Scope接口,该接口在本节中有详细描述。 要了解如何实现自定义作用域,请参阅Spring框架自带的Scope实现以及Scopeopen in new window javadoc,其中更详细地解释了你需要实现的方法。

Scope 接口有四个方法用于从作用域中获取对象、将它们从Scope中移除,以及让对象被销毁。

获取作用域内的对象

例如,会话作用域的实现会返回会话作用域的Bean(如果不存在,则该方法会返回该Bean的新实例,并将其绑定到会话中以供将来引用)。 以下方法返回底层作用域中的对象:

Object get(String name, ObjectFactory<?> objectFactory)

移除作用域内的对象

例如,会话作用域的实现会从底层会话中移除会话作用域的Bean。 应该返回对象,但如果找不到指定名称的对象,则可以返回null。以下方法从底层作用域中移除对象:

Object remove(String name)

注册销毁回调

以下方法注册一个回调(callback),该回调在作用域被销毁 或 作用域中的指定对象被销毁时调用:

void registerDestructionCallback(String name, Runnable destructionCallback)

参阅 javadocopen in new window 或 Spring Scope 的实现,以了解更多关于销毁callback的信息。

获取会话标识符

以下方法获取底层作用域的会话标识符(conversation id):

String getConversationId()

对于每个作用域,这个标识符是不同的。对于会话作用域的实现,这个标识符(id)可以是会话标识符(session id)。

使用自定义 Scope

在编写和测试一个或多个自定义Scope实现之后,你需要让Spring容器知道你的新作用域。 下面的方法是向Spring容器注册新Scope的核心方法:

void registerScope(String scopeName, Scope scope);

该方法声明在ConfigurableBeanFactory接口上,可以通过大多数Spring ApplicationContext实现中的BeanFactory属性访问到。

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

假设你编写了自定义的Scope实现,并按下面的示例进行注册:

下面的示例使用了SimpleThreadScope,它包含在Spring中,但不是默认注册的。对于你自己的自定义Scope实现,注册的步骤是相同的。

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);

接下来可以创建符合你自定义Scope规则的Bean定义,示例如下:

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

使用自定义Scope实现,你不仅可以通过编程方式注册作用域,还可以通过使用CustomScopeConfigurer类进行声明性的作用域注册,示例如下:

<?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/>放置在FactoryBean实现的<bean>声明内部时,作用域的是工厂Bean本身,而不是从getObject()返回的对象。