0%

spring-bean-scope

Spring 框架支持六个作用域,其中四个仅在您使用 Web 感知 ApplicationContext 时可用。

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

表 1. Bean 作用域

Scope Description
singleton (默认)单例
prototype 多例
request 将单个 bean 定义的作用域限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有一个在单个 bean 定义后面创建的 bean 实例。仅在可感知网络的 Spring ApplicationContext中有效。
session 将单个 bean 定义的作用域限定为 HTTP Session的生命周期。仅在可感知网络的 Spring ApplicationContext上下文中有效。
application 将单个 bean 定义的作用域限定为ServletContext的生命周期。仅在可感知网络的 Spring ApplicationContext上下文中有效。
websocket 将单个 bean 定义的作用域限定为WebSocket的生命周期。仅在可感知网络的 Spring ApplicationContext上下文中有效。

从 Spring 3.0 开始,线程作用域可用,但默认情况下未注册。

singleton

定义一个 bean 并且定义其作用域为单例时,Spring IoC 容器将为该 bean 所定义的对象创建一个实例。该单个实例存储在此类单例 bean 的高速缓存中,并且对该 bean 的所有后续请求和引用都返回该高速缓存的对象。下图显示了单例作用域如何工作:

singleton

Singleton 作用域是 Spring 中的默认作用域。要将 bean 定义为 XML 中的单例,可以定义 bean,如以下示例所示:

1
2
3
4
<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

每次对特定 bean 提出请求时,都会导致创建一个新 bean 实例。通常,应将prototype用于所有有状态 Bean,将单例作用域用于无状态bean。

下图说明了 Spring prototype作用域:

prototype

(数据访问对象(DAO)通常不配置为原型,因为典型的 DAO 不拥有任何对话状态。对于我们而言,重用单例图的核心更为容易。)

以下示例将 bean 定义为 XML 原型:

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

因为prototype是多例的模式,所以Spring不负责该bean的整个生命周期,一旦bean被创建,交给client使用,Spring就不会再负责维护该bean实例。

如果在Prototype bean上面配置了生命周期回调方法,那么该方法是不会起作用的。客户端需要自己释放该bean中的资源。

要让Spring容器释放原型作用域bean所拥有的资源,可以使用自定义bean post-processor,用来处理bean的资源清理。

某种意义上Spring的Prototype相当于java中的new方法。

Singleton Bean 依赖 prototype Bean?

既然singleon和prototype的作用域作用域不一样,如果发生singleton Bean需要依赖Prototype的时候,Prototype bean只会被实例化一次,然后注入到singleton bean中。

web 作用域

Request, Session, Application, 和WebSocket作用域仅在使用web的Spring ApplicationContext实现中,如果将这些作用域同Spring正常的IOC容器一起使用,则会报错:IllegalstateException

配置web作用域的方式和普通的应用程序稍有不同。Web程序需要运行在相应的Web容器中,通常我们需要将程序入口配置在web.xml中。

如果你使用了Spring MVC的DispatcherServlet,那么不需要做额外的配置,因为DispatcherServlet已经包含了相关的状态。

  • servlet 2.5 web容器中,是DispatcherServlet之外的的请求,需要注册org.springframework.web.context.request.RequestContextListener
  • servlet 3.0+web容器中,可以使用WebApplicationInitializer接口以编程的方式来添加。
1
2
3
4
5
6
7
8
9
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>

如果Listener不能注册,那么可以注册RequestContextFilter,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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 ,就可以在相应的请求服务中调用相应作用域的bean。

Request scope

考虑以下 XML 配置来定义 bean:

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

Spring容器通过为每个HTTP请求使用LoginAction定义来创建一个新的LoginAction bean实例。也就是说,LoginAction bean的作用域是在HTTP request级别。你可以根据需要更改所创建实例的内部状态,因为从相同的LoginAction bean定义创建的其他实例在状态中看不到这些更改。它们是针对单个请求的。当请求完成处理时,将丢弃该请求的作用域bean。

下面是使用注解@RequestScope的例子:

1
2
3
@RequestScope
@Component
public class LoginAction { // ...}

Session Scope

考虑以下 XML 配置来定义 bean:

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

Spring容器通过在单个HTTP Session的生命周期中使用UserPreferences bean定义创建一个新的UserPreferences bean实例。换句话说,UserPreferences bean在HTTP Session级别有效。

与request scope的bean一样,可以根据需要更改创建的实例的内部状态,因为其他也使用从相同的用户首选项bean定义创建的实例的HTTP session实例在状态中看不到这些更改,因为它们是特定于单个HTTP session的。当最终丢弃HTTP session时,也会丢弃作用于该特定HTTP session的bean。

下面是注解驱动的例子:

1
2
3
@SessionScope
@Component
public class UserPreferences { // ...}

Application Scope

考虑以下 XML 配置来定义 bean:

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

所谓的Application scope就是对于整个web容器来说,bean的作用域是ServletContext级别的,这个和singleton有点类似,但是区别在于,Application scope是ServletContext的单例,singleton是一个ApplicationContext的单例。在一个web容器中ApplicationContext可以有多个。

当然,也可以采用注解的方式来配置:

1
2
3
@ApplicationScope
@Component
public class AppPreferences { // ...}

依赖

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

可以在作用域为单例的 bean 之间使用 <aop:scoped-proxy/>,然后引用通过可序列化的中间代理,因此能够在反序列化时重新获取目标单例 bean。

当针对原型的 bean 声明<aop:scoped-proxy/>时,共享代理上的每个方法调用都会导致创建一个新的目标实例,然后将调用转发到该实例。

此外,作用域代理并不是以生命周期安全的方式从较短范围访问 bean 的唯一方法。您还可以将注入点(即构造函数或 setter 参数或自动装配字段)声明为 ObjectFactory<MyTargetBean>,允许每次需要时调用 getObject() 来检索当前实例 — 无需坚持实例或单独存储。

下例中的配置只有一行,但了解其背后的“为什么”以及“如何”很重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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/>
</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>

要创建这样的代理,请将子 <aop:scoped-proxy/> 元素插入到作用域 bean 定义中(请参阅选择要创建的代理类型和基于 XML 模式的配置)。为什么在请求、会话和自定义范围级别范围内的 bean 定义需要 <aop:scoped-proxy/> 元素?考虑以下单例 bean 定义,并将其与您需要为上述范围定义的内容进行对比(请注意,以下 userPreferences bean 定义不完整):

1
2
3
4
5
6
7
8
9
10
11
<!-- an HTTP Session-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.flydean.service.UserPreferences" scope="session">
<!-- instructs the container to proxy the surrounding bean -->
<aop:scoped-proxy/>
</bean>

<!-- a singleton-scoped bean injected with a proxy to the above bean -->
<bean id="userService" class="com.flydean.service.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>

在前面的示例中,单例 bean (userManager) 注入了对 HTTP 会话范围 bean (userPreferences) 的引用。这里的重点是 userManager bean 是一个单例:它在每个容器中被实例化一次,并且它的依赖项(在这种情况下只有一个,userPreferences bean)也只注入一次。这意味着 userManager bean 只对完全相同的 userPreferences 对象(即最初注入它的那个对象)进行操作。

这不是您将生命周期较短的作用域 bean 注入到生命周期较长的作用域 bean(例如,将 HTTP 会话作用域的协作 bean 作为依赖项注入到单例 bean 中)时想要的行为。相反,您需要一个 userManager 对象,并且,对于 HTTP 会话的生命周期,您需要一个特定于 HTTP 会话的 userPreferences 对象。因此,容器创建一个对象,该对象公开与 UserPreferences 类完全相同的公共接口(理想情况下是一个 UserPreferences 实例的对象),它可以从作用域机制(HTTP 请求、会话等)中获取真正的 UserPreferences 对象.容器将这个代理对象注入到 userManager bean 中,它不知道这个 UserPreferences 引用是一个代理。在这个例子中,当一个 UserManager 实例在依赖注入的 UserPreferences 对象上调用一个方法时,它实际上是在调用代理上的一个方法。然后代理从(在这种情况下)HTTP 会话中获取真实的 UserPreferences 对象,并将方法调用委托给检索到的真实 UserPreferences 对象。

因此,在将请求范围和会话范围的 bean 注入协作对象时,您需要以下(正确且完整的)配置,如以下示例所示:

1
2
3
4
5
6
7
8
<!-- DefaultUserPreferences implements the UserPreferencesInterface interface -->
<bean id="userPreferencesC" class="com.flydean.service.DefaultUserPreferences" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>

<bean id="userManager" class="com.flydean.service.UserManager">
<property name="userPreferences" ref="userPreferencesC"/>
</bean>

自定义作用域

Spring提供了一个org.springframework.beans.factory.config.Scope接口来实现自定义作用域的功能。

Spring3.0开始,提供了thread的作用域,但是这个作用域需要自己来注册。 我们来看Spring自己的SimpleThreadScope是怎么定义和使用的。

首先 SimpleThreadScope 实现了 Scope接口, Scope接口提供了5个方法:

  • Object get(String name, ObjectFactory<?> objectFactory); 从所在作用域返回对象。
  • Object remove(String name); 从作用域删除对象
  • void registerDestructionCallback(String name, Runnable callback); 注册销户回调方法
  • Object resolveContextualObject(String key); 根据key获得上下文对象
  • String getConversationId(); 获得当前scope的会话ID

自定义好了Scope类之后,需要将其注册到Spring容器中,可以通过大多数Spring ApplicationContext 的ConfigurableBeanFactory接口来注册:

1
void registerScope(String scopeName, Scope scope);

下面是编程方式的注册:

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

下面是配置方式的注册:

1
2
3
4
5
6
7
8
9
<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>