spring framework 4 学习之路 -- bean scopes

       当你创建bean定义时,你就创建了bean定义中类实例创建的方法。bean定义实际上是一个菜谱的概念是十分重要的,因为,这意味着对于一个类,你可以通过一个菜谱创建多个对象。

       你不仅可以控制各种各样的依赖和配置值,这些依赖和配置值连接从特定bean定义中创建的对象;同时,你也可以控制特定bean定义中创建的对象的范围(scope)。这种方式是非常有用的,灵活的,你可以选择你通过配置创建的对象的范围,而不用在Java类级别上处理对象的范围。Beans可以定义使用多种范围中的一个:Spring Framework支持7种装箱即用的范围,当你使用web层的ApplicationContext时,只有5种范围是可用的。以下的范围都是支持装箱即用的。你也可以创建custom范围。

spring framework 4 学习之路 -- bean scopes

1. singleton scope

       单例bean的实例只有一个,所有通过id或者ids请求的beans只会得到一个特定的bean实例,该bean实例通过Spring容器返回。当你声明bean定义,同时将它设置为单例bean,那么Spring容器会根据该bean定义创建一个对象实例。这个单独的实例被存储在专门存放单例beans的缓存中,所有对该bean的请求或者引用都会返回缓存中存放的那个对象。

spring framework 4 学习之路 -- bean scopes

       Spring关于单例bean的概念不同于Gang of Four(GoF)模式书中定义的单例模式。GoF单例模式硬编码对象的范围,对于每个类加载器,只有一个特定类的对象会被创建。Spring的单例范围最好的描述是单个容器,单个bean。这意味着,如果你在单个Spring容器中定义针对某一个类的bean,那么Spring容器创建有且仅有一个该类的实例。在Spring中,单例范围是默认的范围配置。如果你需要在XML中定义bean为单例bean,可以使用如下的方式:

spring framework 4 学习之路 -- bean scopes

2. prototype scope

      bean的非单例,原型范围配置会导致每次对该bean请求时,都会创建bean的新实例对象。也就是说,bean被注入到另一个bean中,或者你通过容器的getBean()方法请求它。作为一个规则,对所有具有状态的beans使用原型范围,而对于无状态的beans则使用单例范围。

       下述的图表描述了Spring的原型范围。数据获取对象(DAO)一般不会配置成原型范围,因为一般的DAO并不维持任何的状态。它更适合单例范围。

spring framework 4 学习之路 -- bean scopes

     下述的例子在XML中定义bean的范围为原型范围:

spring framework 4 学习之路 -- bean scopes

       与其他范围不同,Spring并不管理原型范围的bean的完整生命周期:容器初始化,配置,装配原型范围的对象,将它返回给客户端,除此之外,并没有其他更多关于原型范围实例的更多记录。因此,尽管初始化生命周期回调方法会在所有的对象上进行调用,而不管每个对象的范围,但是,对于原型范围,配置销毁生命周期的回调方法并不会被调用。客户端代码必须清理原型范围的对象,并且释放原型范围对象所持有的重要资源。为了让Spring容器能够释放由原型范围beans持有的资源,尝试使用公共的bean post-processor, 该bean post-processor持有需要被清理的beans的引用。

        在某些方面,Spring容器对于原型范围的bean的角色相当于Java new操作的替换。所有关于生命周期的管理交由客户端代码处理。

3. 单例beans依赖原型bean

       当你使用依赖prototype beans的单例beans时,意识到依赖是在初始化阶段解决的。因此,如果你要将一个原型范围的bean注入到单例bean中,一个新的原型范围bean会被初始化,然后注入到单例bean中。原型实例是一个决定性实例应用到单例范围的bean中。

        但是,如果你希望在运行阶段,单例bean可重复的获取原型范围bean的新实例,那么,你不能依赖注入原型范围bean到单例范围bean中,因为依赖注入只发生一次,只会在Spring容器初始化单例bean,并解决bean的依赖过程中产生。如果你希望在运行期间,多次获取原型范围bean的实例,可以参考上一文bean依赖中方法注入章节。

4. Request, session, global session, application, WebSocket范围

      只有当你使用web级别的Spring ApplicationContext实现(比如XmlWebApplicationContext),request, session, global session, application和websocket范围才是可用的。如果你在常规的Spring IoC容器中,比如ClassPathXmlApplicationContext,使用这些范围,那么将会抛出IllegalStateException异常。

4.1 初始化web配置

       为了支持beans在request, session, globalSession, application以及websocket级别的范围配置,在定义beans之前,一些镜像初始化配置是需要的。

       你如何完成初始化启动依赖于特定的Servlet环境。

       如果你通过Spring Web MVC获取范围beans,实际上请求是由Spring DispatcherServlet或者DispatcherPortlet处理的,那么,不需要特别的启动: DispatcherServlet和DispatcherPortlet已经暴露了所有相关的状态。

      如果你使用Servlet 2.5 web容器,请求都是在Spring的DispatcherServlet之外处理的,那么你要注册ServletRequestListener (org.springframework.web.context.request.RequestContextListener.ServletRequestListener)。对于Servlet 3.0+,这些可以通过WebApplicationInitializer接口进行处理。对于更老的容器,在web应用的web.xml文件中添加如下的声明:

spring framework 4 学习之路 -- bean scopes

       如果在listener启动时出现问题,考虑使用Spring的RequestContextFilter。过滤器映射依赖于web应用配置,因此你必须使用合适的方式。

spring framework 4 学习之路 -- bean scopes

      DispatcherServlet, RequestContextListener以及RequestContextFilter实际上都是做相同的事情,即是绑定HTTP请求对象到Thread,该Thread用来处理该请求。这些使得request和session范围的beans能够在整个调用链中可获取。

4.2 Request范围

        考虑如下XML配置中关于bean的定义:

spring framework 4 学习之路 -- bean scopes

        Spring容器创建loginAction bean的对象实例,通过对每个HTTP请求使用loginAction bean定义。也就是说,loginAction bean的范围定义是在HTTP请求级别的。你可以改变实例的内部状态,这些实例你想创建多少就创建多少,因为通过相同loginAction bean定义创建的其他实例无法查看这些状态的变化;每个独立的请求彼此之间是隔离的。当请求完成了,request范围的bean也就被废弃了。

         当使用基于注解的组件或者Java配置时,注解@RequestScope可以被用来指定组件为request范围。

spring framework 4 学习之路 -- bean scopes

4.3 Session范围

       考虑如下的XML配置中bean定义:

spring framework 4 学习之路 -- bean scopes

      Spring容器在一个单独的HTTP Session生命周期内使用userPreferences bean定义来创建UserPreferences新实例。也就是说,userPreferences bean的范围是定义在HTTP会话级别的。正如request范围的bean,你可以改变实例的内部状态,这些实例你想创建多少就创建多少,其他HTTP会话实例同样使用相同的userPreferences bean定义来创建实例,其他HTTP会话实例无法看到该会话实例的状态变化,因为对于每一个HTTP会话,它们都是独立的。当HTTP会话最终被废弃了,定义在HTTP会话范围内的bean也同样被废弃掉了。

        如果使用基于注解的组件或者Java配置,注解@SessionScope可以被用来指定组件的范围为session范围。

spring framework 4 学习之路 -- bean scopes

4.4 Global session范围

       考虑如下的XML配置中bean定义:

spring framework 4 学习之路 -- bean scopes

       globalSession范围类似于标准的HTTP会话范围,只应用在portlet-based web应用中。portlet明确定义global session的概念。范围为global session的bean可以在所有portlets之间共享,这些portlets构成了一个完整的portlet web应用。Beans定义为global session范围与全局的portlet session生命周期一致。

      如果你编写标准的基于Servlet的web应用,你可以定义一个或多个beans的范围为globalSession范围,那么标准的HTTP Session范围会被应用,不会出现任何错误。

4.5 Application范围

       考虑如下XML配置中关于bean的定义:

spring framework 4 学习之路 -- bean scopes

     Spring容器在整个web应用阶段通过使用appPreferences bean定义创建AppPreferences实例一次。也就是说,bean appPreferences的范围定义是在ServletContext级别上的,作为一个常规的ServletContext属性存储。这个有一点类似于Spring的单例bean,但是这两种范围有一些不同:每个ServletContext是单例的,不同于每个Spring ApplicationContext是单例的; 作为一个ServletContext属性,这些bean会被暴露,因此是可见的。

       当你使用基于注解的组件或者Java配置,注解@ApplicationScope可以被用来指定组件为application范围。

spring framework 4 学习之路 -- bean scopes

4.6 指定范围的beans作为依赖

       Spring IoC容器不仅管理对象的初始化,同时也装配协作者。如果你想注入一个HTTP请求范围的bean到另一个更长生命周期的bean中,你可以选择在范围bean的地方注入AOP代理。也就是说,你需要注入一个代理对象,该代理对象关于范围对象暴露了相同的公共接口,同时也可以根据相关的范围检索目标对象,在真实的对象上委托方法调用。

       你也可以在单例beans之间使用<aop: scoped-proxy/>,引用可以通过中间代理被序列化,因此能够在反序列化时重新获取目标单例。当针对prototype范围的bean声称<aop:scoped-proxy/>,每一次在共享代理上的方法调用都会导致创建新的实例。当然,范围代理并不是以安全的方式从更短的生命周期中获取beans的唯一方法。你可以声明你的依赖点作为ObjectFactory<MyTargetBean>,允许通过调用getObject()方法来检索当前的实例。

        如下的例子展示了上述情况的一种实现方式,但是更为重要的是要理解why和how。

spring framework 4 学习之路 -- bean scopes

      为了创建这样一个代理,你可以在短范围bean的定义中加入子标签<aop:scoped-proxy/>。为什么我们在定义request, session, globalSession以及自定义范围的bean时需要引入<aop:scoped-proxy/>标签?让我们来看下如下的单例bean定义,并且比较下单例bean和前面提到的范围bean的不同点。

spring framework 4 学习之路 -- bean scopes

      在上述的例子中,单例bean userManager依赖一个HTTP Session范围内的bean userPreferences。这里的主要点在于bean userManager是单例的:它只会被容器初始化一次,它的依赖也只会被注入一次。这就意味着bean userManager只能操作一个userPreferences对象,这个对象是在容器初始化的过程中注入的。

       当你把短生命周期的bean注入到长生命周期的bean中,这种行为并不是你所期望的,比如说,注入HTTP Session范围的协作bean到单例bean中。实际上,你需要一个单例userManager对象和一个针对HTTP Session范围的userPreferences对象。因此容器通过暴露了关于userPreferences类创建实例的公共接口来创建对象,而该接口可以根据范围机制创建真正的userPreferences对象。容器注入代理对象到userManager bean中,但是userManager并不会意识到userPreferences引用是一个代理。在这个例子中,当一个userManager实例调用UserPreferences对象上的方法,它实际上调用的是代理的方法。代理从HTTP Session中获取实际的对象,然后委托方法调用到真正对象上。

        因此,当你注入request-, session-以及globalSession-scoped beans作为协作beans时,你需要以下正确的,完整的配置。

spring framework 4 学习之路 -- bean scopes

4.7 选择代理的类型来创建

       默认情况下, 当你在bean定义中标记了<aop:scoped-proxy/>标签,Spring容器创建bean的代理,该代理类型是基于CGLIB类的代理。

        可选择地,你可以配置Spring容器为这些范围beans创建基于标准JDK接口的代理,通过指定<aop:scoped-proxy/>标签的proxy-target-class属性为false。使用基于JDK接口的代理意味着你在应用路径上不用引入额外的类库。但是,这也意味着范围bean的类至少实现一个接口,而且注入该范围bean的所有beans必须通过它的接口引用该bean。

spring framework 4 学习之路 -- bean scopes

5.自定义范围

       bean的范围是可扩展的。你可以定义你自己的范围,或者重新定义已经存在的范围,尽管后者一般不被建议。另外,单例范围和原型范围是不可重载的。

5.1 创建自定义范围

        为了整合自定义范围到Spring容器中,你需要实现org.springframework.beans.factory.config.Scope接口。关于如何实现你自己的范围,可以参考Spring Framework已经实现的范围,也可以查看javadocs,javadocs详细解释了你需要实现的方法。

        Scope接口提供了四种方法从范围内获取对象,从范围内移除对象,允许对象被销毁。

       如下的方法返回指定范围内的对象。比如说,session范围实现会返回session范围的bean。如果指定的bean不存在,方法会创建新对象,并将该新对象和当前session做绑定。

spring framework 4 学习之路 -- bean scopes

       如下的方法从当前范围内删除对象。比如说,session范围实现会从当前session中删除session范围bean。对象应当被返回,但是如果指定bean不存在,那么会返回null。

spring framework 4 学习之路 -- bean scopes

        如下的方法注册当指定范围被销毁或者指定范围内的对象被销毁时,应当被执行的回调方法。查看Spring scope的实现或者javadoc文档了解更多关于销毁回调方法。

spring framework 4 学习之路 -- bean scopes

spring framework 4 学习之路 -- bean scopes        如下的方法获取当前范围内的会话标识符。这个标识符对于每一个范围都是唯一的。对于session范围的实现,这个标识符就会session的标识符。

spring framework 4 学习之路 -- bean scopes

5.2 使用自定义的范围

        当你编写并且测试了一个或多个自定义范围的实现,你需要让容器能够识别到你新创建的范围。如下的方法是注册一个新的范围到Spring容器的核心方法:

spring framework 4 学习之路 -- bean scopes

        这个方法声明在ConfigurableBeanFactory接口里,这个接口在大多数的ApplicationContext实现类里都存在。

      registerScope方法的第一个参数是与范围相关的唯一名称,比如在Spring容器中有范围名称singleton和prototype。registerScope方法的第二个参数就是自定义范围的具体实现对象。

        假定你编写了一个自定义范围实现,然后使用如下的方法进行注册。(以下的例子SimpleThreadScope已经在Spring中包含了,但是默认没有进行注册。)

spring framework 4 学习之路 -- bean scopes

       然后你可以创建一个bean,使用上述自定义的范围。

spring framework 4 学习之路 -- bean scopesspring framework 4 学习之路 -- bean scopes

       对于自定义范围实现,不一定要通过代码进行注册。你也可以使用声明式的范围注册,通过使用CustomScopeConfigurer类,如下所示:

spring framework 4 学习之路 -- bean scopesspring framework 4 学习之路 -- bean scopes