1.5 bean范围

您不仅可以控制要插入到bean定义中的各种依赖项和配置值,还可以控制从bean定义创建的对象的范围。这种方法是强大和灵活的,因为您可以通过配置来选择您创建的对象的范围,而不必在Java类级别的对象范围内进行设置。bean可以定义到Spring框架支持六个作用域,其中四个作用域仅在使用Web环境下的ApplicationContext时可用。当然还可以创建你自己的定义范围。
下表介绍了支持的作用域:

表3 Bean的作用域:

Scope 说明
singleton (默认)单例对象实例。
prototype 将单个bean定义范围扩展到任意数量的对象实例。调用时就新建
request 将单个bean定义限定到单个HTTP请求的生命周期。也就是说,每个HTTP请求都有自己的bean实例,该实例是在单个bean定义的后面创建的。仅在Web环境的Spring ApplicationContext中有效。
session 将单个bean定义范围扩展到HTTP会话的生命周期。 仅在Web环境的Spring ApplicationContext中有效。
application 将单个bean定义范围扩展到ServletContext的生命周期. 仅在Web环境的Spring ApplicationContext中有效。
websocket 将单个bean定义范围扩展到WebSocket的生命周期.仅在Web环境的Spring ApplicationContext中有效.

从Spring3.0开始,线程作用域可用,但默认情况下不注册:请参阅SimpleThreadScope。从Spring4.2开始,事务范围也可用:SimpleTransactionScope。有关如何注册这些或任何其他自定义范围的说明,请参阅使用自定义范围。

1.5.1.单例对象范围

当您定义一个bean定义并且它的作用域是一个单例时,Spring IOC容器只创建由该bean的一个实例。这个单例存储在单实例bean的缓存中,所有随后的请求和对这个名为bean的引用都返回缓存的对象。下图显示了单例作用域的工作方式:

要在XML中将bean定义为singleton,可以定义bean,如下例所示:

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

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

prototype范围

每次请求特定bean时,非单例bean都会创建一个新的bean实例。也就是说,bean被注入到另一个bean中,或者通过对容器的getBean()方法调用请求它,都会创建。通常,您应该为所有有状态Bean使用prototype作用域,为无状态bean使用单例作用域。

下图说明了Spring prototype范围:

数据访问对象(DAO)通常不配置为原型,因为典型的DAO不具有任何会话状态。
下面的示例将bean定义为XML中的原型:

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

与其他范围不同,Spring不管理prototype Bean的完整生命周期。容器实例化、配置并以其他方式组装prototype 对象,然后将其交给客户端,而不再记录该实例。因此,尽管所有对象在初始化生命周期都调用回调方法,但在原型的情况下,不会调用配置的销毁生命周期的方法。客户端代码必须清理prototype对象,并释放prototype bean所拥有的资源。要让Spring容器释放prototype bean所拥有的资源,请尝试使用自定义bean 的post-processor处理器,该处理器包含对需要清理的bean的引用。
在某些方面,Spring容器对prototype bean中的作用就是替代Java的new操作。超过这一点的所有生命周期管理都必须由客户端处理。(有关Spring容器中bean生命周期的详细信息,请参阅生命周期回调。)

1.5.3. 与prototype bean有依赖关系的单例bean

如果您将一个prototype范围的bean注入到一个单例范围的bean中,那么将实例化一个新的原型bean,然后将依赖项注入到单例bean中。原型实例是提供给单例作用域bean的唯一实例。

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

1.5.4 Request, Session, Application,和WebSocket范围

request, session, application, 和websocket这些仅在使用Web环境的Spring ApplicationContext实现(如XmlWebApplicationContext)时可用。如果将这些作用域与常规的Spring IOC容器(如ClassPathXmlApplicationContext)一起使用,则会引发一个IllegalstateException,它会显示未知的bean作用域。

初始Web配置
要支持request, session, application, 和websocket作用域,在定义bean之前需要一些小的初始配置。(对于标准范围:singleton和prototype,不需要此初始设置。)
如何完成这个初始设置取决于您的特定servlet环境。
如果您在Spring Web MVC中访问作用域bean,实际上,在SpringDispatcherServlet处理的请求中,不需要特殊设置。DispatcherServlet已公开所有相关状态。
如果使用servlet 2.5 Web容器,并且请求在Spring的DispatcherServlet之外处理(例如,使用JSF或Struts时),则需要注册org.springframework.web.context.request.RequestContextListener servletrequestListener。对于servlet 3.0+,这可以通过使用WebApplicationInitializer接口以编程方式完成。或者,对于较旧的容器,将以下声明添加到Web应用程序的web.xml文件中:

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

或者,如果监听器设置有问题,请考虑使用Spring的RequestContextFilter。过滤器映射取决于周围的Web应用程序配置,因此您必须根据需要更改它。下面的列表显示了Web应用程序的filter部分:

<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请求对象绑定到为该请求提供服务的线程。这使得请求和会话范围的bean可以在调用链中进一步使用。

request范围

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

等价JAVA中:

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

session范围

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

等价JAVA中:

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

application范围

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

等价JAVA中:

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

作用域bean作为依赖项
SpringIOC容器不仅管理对象(bean)的实例化,还管理依赖关系。如果要将HTTP请求作用域bean注入耗时较长作用域的另一个bean中,可以选择插入AOP代理来代替作用域bean。也就是说,您需要插入一个代理对象,该对象与作用域对象公开相同的公共接口,但也可以从相关作用域(如HTTP请求)中检索实际目标对象,并将方法调用委托给实际对象。
以下示例中的配置仅为一行,但了解其背后的“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/> 
    </bean>

    <!--一个单例bean注入前面定义的代理bean -->
    <bean id="userService" class="com.something.SimpleUserService">
        <!-- 引用代理的userPreferences bean -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>

要创建这样的代理,可以将一个子<aop:scoped proxy/>元素插入到作用域bean定义中。为什么在请求、会话和自定义范围级别定义范围的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>

在前面的示例中,单例userManager被注入session的bean(userPreferences)的引用。这里的突出点是,UserManager bean是一个单例:每个容器只实例化一次,它的依赖项(在本例中只有一个,即UserPreferences bean)也只注入一次。这意味着userManager bean只在完全相同的userPreferences对象(即最初注入它的对象)上操作。

这不是将较短寿命的bean注入寿命较长的作用域bean时需要的行为。因此,您需要一个UserManager对象,并且在HTTP会话的生命周期中,您需要一个特定于HTTP会话的UserPreferences对象。因此,容器创建一个对象,该对象与UserPreferences类公开完全相同的公共接口(理想情况下是一个UserPreferences实例的对象),该对象可以从作用域机制(HTTP请求、会话等)获取实际的UserPreferences对象。容器将这个代理对象注入到UserManager bean中,后者不知道这个引用是代理。在本例中,当一个UserManager实例在依赖项注入的UserPreferences对象上调用一个方法时,它实际上是调用代理上的一个方法。然后,代理从HTTP会话(在本例中)中获取真实 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代理只拦截公共方法调用!不要在此类代理上调用非公共方法。它们不会委托给实际作用域目标对象。

或者,可以通过为<aop:scoped proxy/>元素的proxy-target-class属性的值指定false,将spring容器将创建标准的基于JDK接口的代理。使用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>