Spring核心技术(五)——Spring中Bean的做用域

前文概述了Spring的容器,Bean,以及依赖的一些信息,本文将描述一下Bean的做用域javascript

Bean的做用域

当开发者定义Bean的时候,同时也会定义了该如何建立Bean实例。这些具体建立的过程是很重要的,由于只有经过对这些过程的配置,开发者才能建立实例对象。html

开发者不只能够控制注入不一样的依赖到Bean之中,也能够配置Bean的做用域。这种方法是很是强大并且弹性也很是好的。开发者能够经过配置来指定对象的做用域,而不用在Java类层次上来配置。Bean能够配置多种做用域。
Spring框架支持5种做用域,有三种做用域是当开发者使用基于web的ApplicationContext的时候才生效的。java

下面就是Spring直接支持的做用域了,固然开发者也能够本身定制做用域。web

做用域 描述
单例(singleton) (默认)每个Spring IoC容器都拥有惟一的一个实例对象
原型(prototype) 一个Bean定义,任意多个对象
请求(request) 一个HTTP请求会产生一个Bean对象,也就是说,每个HTTP请求都有本身的Bean实例。只在基于web的Spring ApplicationContext中可用
会话(session) 限定一个Bean的做用域为HTTPsession的生命周期。一样,只有基于web的Spring ApplicationContext才能使用
全局会话(global session) 限定一个Bean的做用域为全局HTTPSession的生命周期。一般用于门户网站场景,一样,只有基于web的Spring ApplicationContext可用
应用(application) 限定一个Bean的做用域为ServletContext的生命周期。一样,只有基于web的Spring ApplicationContext可用

在Spring 3.0中,线程做用域是可用的,但不是默认注册的。想了解更多的信息,能够参考本文后面关于SimpleThreadScope的文档。想要了解如何注册这个或者其余的自定义的做用域,能够参考后面的内容。spring

单例Bean

单例Bean全局只有一个共享的实例,全部将单例Bean做为依赖的状况下,容器返回将是同一个实例。编程

换言之,当开发者定义一个Bean的做用域为单例时,Spring IoC容器只会根据Bean定义来建立该Bean的惟一实例。这些惟一的实例会缓存到容器中,后续针对单例Bean的请求和引用,都会从这个缓存中拿到这个惟一的实例。设计模式

Spring的单例Bean和与设计模式之中的所定义的单例模式是有所区别的。设计模式中的单例模式是将一个对象的做用域硬编码的,一个ClassLoader只有惟一的一个实例。
而Spring的单例做用域,是基于每一个容器,每一个Bean只有一个实例。这意味着,若是开发者根据一个类定义了一个Bean在单个的Spring容器中,那么Spring容器会根据Bean定义建立一个惟一的Bean实例。
单例做用域是Spring的默认做用域,下面的例子是在基于XML的配置中配置单例模式的Bean。缓存

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

<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>

原型Bean

非单例的,原型的Bean指的就是每次请求Bean实例的时候,返回的都是新实例的Bean对象。也就是说,每次注入到另外的Bean或者经过调用getBean()来得到的Bean都将是全新的实例。
这是基于线程安全性的考虑,若是使用有状态的Bean对象用原型做用域,而无状态的Bean对象用单例做用域。安全

下面的例子说明了Spring的原型做用域。DAO一般不会配置为原型对象,由于典型的DAO是不会有任何的状态的。markdown

下面的例子展现了XML中如何定义一个原型的Bean:

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

与其余的做用域相比,Spring是不会彻底管理原型Bean的生命周期的:Spring容器只会初始化,配置以及装载这些Bean,传递给Client。可是以后就不会再去管原型Bean以后的动做了。
也就是说,初始化生命周期回调方法在全部做用域的Bean是都会调用的,可是销毁生命周期回调方法在原型Bean是不会调用的。因此,客户端代码必须注意清理原型Bean以及释放原型Bean所持有的一些资源。
能够经过使用自定义的bean post-processor来让Spring释放掉原型Bean所持有的资源。

在某些方面来讲,Spring容器的角色就是取代了Java的new操做符,全部的生命周期的控制须要由客户端来处理。

单例Bean依赖原型Bean

当使用单例Bean的时候,而该Bean的依赖是原型Bean的时候,须要注意的是依赖的解析都是在初始化的阶段的。所以,若是将原型Bean注入到单例的Bean之中,只会请求一次原型的Bean,而后注入到单例的Bean之中。这个依赖的原型Bean仍然属于只有一个实例的。

然而,假设你须要单例Bean对原型的Bean的依赖须要每次在运行时都请求一个新的实例,那么你就不可以将一个原型的Bean来注入到一个单例的Bean当中了,由于依赖注入只会进行一次。当Spring容器在实例化单例Bean的时候,就会解析以及注入它所需的依赖。若是实在须要每次都请求一个新的实例,能够参考Spring核心技术IoC容器(四)中的方法注入部分。

请求,会话,全局会话的做用域

request,session以及global session这三个做用域都是只有在基于web的SpringApplicationContext实现的(好比XmlWebApplicationContext)中才能使用。
若是开发者仅仅在常规的Spring IoC容器中好比ClassPathXmlApplicationContext中使用这些做用域,那么将会抛出一个IllegalStateException来讲明使用了未知的做用域。

Web初始化配置

为了可以使用request,session以及global session做用域(web范围的Bean),须要在配置Bean以前配置作一些基础的配置。(对于标准的做用域,好比singleton以及prototype,是无需这些基础的配置的)

具体如何配置取决于Servlet的环境。

好比若是开发者使用了Spring Web MVC框架的话,每个请求会经过Spring的DispatcherServlet或者DispatcherPortlet来处理的,也就没有其余特殊的初始化配置。DispatcherServletDispatcherPortlet已经包含了相关的状态。

若是使用Servlet 2.5的web容器,请求不是经过Spring的DispatcherServlet(好比JSF或者Struts)来处理。那么开发者须要注册org.springframework.web.context.request.RequestContextListener或者ServletRequestListener
而在Servlet 3.0之后,这些都可以经过WebApplicationInitializer接口来实现。或者,若是是一些旧版本的容器的话,能够在web.xml中增长以下的Listener声明:

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

若是是对Listener不甚熟悉,也能够考虑使用Spring的RequestContextFilter。Filter的映射取决于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>

DispatcherServletRequestContextListener以及RequestContextFilter作的本质上彻底一致,都是绑定request对象到服务请求的Thread上。这才使得Bean在以后的调用链上在请求和会话范围上可见。

请求做用域

参考以下的Bean定义

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

Spring容器会在每次用到loginAction来处理每一个HTTP请求的时候都会建立一个新的LoginAction实例。也就是说,loginActionBean的做用域是HTTPRequest级别的。
开发者能够随意改变实例的状态,由于其余经过loginAction请求来建立的实例根本看不到开发者改变的实例状态,全部建立的Bean实例都是根据独立的请求来的。当请求处理完毕,这个Bean也会销毁。

会话做用域

参考以下的Bean定义:

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

Spring容器会在每次调用到userPreferences在一个单独的HTTP会话周期来建立一个新的UserPreferences实例。换言之,userPreferencesBean的做用域是HTTPSession级别的。
request-scoped做用域的Bean上,开发者能够随意的更改实例的状态,一样,其余的HTTPSession基本的实例在每一个Session都会请求userPreferences来建立新的实例,因此开发者更改Bean的状态,对于其余的Bean仍然是不可见的。当HTTPSession销毁了,那么根据这个Session来建立的Bean也就销毁了。

全局会话做用域

该部分主要是描述portlet的,详情能够Google更多关于portlet的相关信息。

参考以下的Bean定义:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="globalSession"/>

global session做用域比较相似以前提到的标准的HTTPSession,这种做用域是只应用于基于门户(portlet-based)的web应用的上下之中的。门户的Spec中定义的global session的意义:global session被全部构成门户的web应用所共享。定义为global session做用域的BEan是做用在全局门户Session的声明周期的。

若是在使用标准的基于Servlet的Web应用,并且定义了global session做用域的Bean,那么只是会使用标准的HTTPSession做用域,不会报错。

应用做用域

考虑以下的Bean定义:

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

Spring容器会在整个web应用使用到appPreferences的时候建立一个新的AppPreferences的实例。也就是说,appPreferencesBean是在ServletContext级别的,好似一个普通的ServletContext属性同样。这种做用域在一些程度上来讲和Spring的单例做用域是极为类似的,可是也有以下不一样之处:

  • application做用域是每一个ServletContext中包含一个,而不是每一个SpringApplicationContext之中包含一个(某些应用中可能包含不止一个ApplicationContext)。
  • application做用域仅仅做为ServletContext的属性可见,单例Bean是ApplicationContext可见。

做为依赖

Spring IoC容器不只仅管理对象(Bean)的实例化,同时也负责装载依赖。若是开发者想装载一个Bean到一个做用域更广的Bean当中去(好比HTTP请求返回的Bean),那么开发者选择注入一个AOP代理而不是短做用域的Bean。也就是说,开发者须要注入一个代理对象,这个代理对象既能够找到实际的Bean,也可以建立一个全新的Bean。

开发者会在单例Bean中使用<aop:scoped-proxy/>标签,来引用一个代理,这个代理的做用就是用来获取指定的Bean。
当生命使用<aop:scoped-proxy/>来生成一个原型Bean的时候,每一个经过代理的调用都会产生一个新的目标实例。
而且,做用域代理并非惟一来获取短做用域Bean的惟一安全的方式。开发者也能够经过简单的声明注入为ObjectFactory<MyTargetBean>,别容许经过蕾西getObject()之类的调用来获取一些指定的依赖,而不是单独储存依赖的实例。
JSR-330关于这部分的不一样叫作
Provider,经过使用Provider声明和一个相关的get()方法来获取指定的依赖。详细关于JSR-330的信息能够进去详细了解。

请参考下面的例子:

<?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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- an HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.foo.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.foo.SimpleUserService">
        <!-- a reference to the proxied userPreferences bean -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>

使用代理,只须要在短做用域的Bean定义之中加入一个子节点<aop:scoped-proxy/>便可。Spring核心技术IoC容器(四)中的方法注入中就说起到了Bean依赖的一些问题,这也是咱们为何要使用aop代理的缘由。假设咱们没有使用aop代理而是直接进行依赖注入,参考以下的例子:

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

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

上面的例子中,userManager明显是一个单例的Bean,注入了一个HTTPSession级别的userPreferences依赖,显然的问题就是userManager在Spring容器中只会实例化一次,而依赖(当前例子中的userPreferences)也只能注入一次。这也就意味着userManager每次使用的都是相同的userPreferences对象。

那么这种状况就绝对不是开发者想要的那种将短做用域注入到长做用域Bean中的状况了,举例来讲,注入一个HTTPSession级别的Bean到一个单例之中,或者说,当开发者经过userManager来获取指定与某个HTTPSessionuserPreferences对象都是不可能的。因此容器建立了一个获取UserPreferences对象的接口,这个接口能够根据Bean对象做用域机制来获取与做用域相关的对象(好比说HTTPRequest或者HTTPSession等)。容器以后注入代理对象到userManager中,而意识不到所引用UserPreferences是代理。在这个例子之中,当UserManager实例调用方法来获取注入的依赖UserPreferences对象时,其实只会调用了代理的方法,由代理去获取真正的对象,在这个例子中就是HTTPSession级别的Bean。

因此当开发者但愿可以正确的使用配置request,session或者globalSession级别的Bean来做为依赖时,须要进行以下的相似配置:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
    <aop:scoped-proxy/>
</bean>

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

选择代理的类型

默认状况下,Spring容器建立代理的时候标记为<aop:scoped-proxy/>的标签时,会建立一个基于CGLIB的代理

CGLIB代理会拦截public方法调用!因此不要在非public方法上使用代理,这样将不会获取到指定的依赖。

或者,开发者能够经过指<aop:scoped-proxy/>标签的proxy-target-class属性的值为false来配置Spring容器来为这些短做用域的Bean建立一个标准JDK的基于接口的代理。使用JDK基于接口的代理意味着开发者不须要在应用的路径引用额外的库来完成代理。固然,这也意味着短做用域的Bean须要额外实现一个接口,而依赖是从这些接口来获取的。

<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.foo.DefaultUserPreferences" scope="session">
    <aop:scoped-proxy proxy-target-class="false"/>
</bean>

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

DefaultUserPreferences实现了UserPreferences并且提供了接口来获取实际的对象。更多的信息能够参考AOP代理

定制做用域

Bean的做用域机制是可扩展的,开发者能够定义本身的一些做用域,甚至从新定义已经存在的做用域,可是这一点Spring团队是不推荐的,而且开发者不可以重写singleton以及prototype做用域。

建立定制做用域

为了可以使Spring能够管理开发者定义的做用域,开发者须要实现org.springframework.beans.factory.config.Scope接口。想知道如何实现开发者本身定义的做用域,能够参考Spring框架的一些实现或者是Scope的javadoc,里面会解释开发者须要实现的一些细节。

Scope接口中含有4个方法来获取对象,移除对象,容许销毁等。

下面的方法返回一个存在的做用域的对象。好比说Session的做用域实现,该函数将返回会话做用域的Bean(若是Bean不存在,该方法会建立一个新的实例)

Object get(String name, ObjectFactory objectFactory)

下面的方法会将对象移出做用域。一样,以Session为例,该函数会删除Session做用域的Bean。删除的对象会做为返回值返回,当没法找到对象的时候能够返回null

Object remove(String name)

下面的方法会注册一个回调方法,当须要销毁或者做用域销毁的时候调用。详细能够参考在javadoc和Spring做用域的实现中找到更多关于销毁回调方法的信息。

void registerDestructionCallback(String name, Runnable destructionCallback)

下面的方法会获取做用域的区分标识,区分标识区别于其余的做用域。

String getConversationId()

使用定制做用域

在实现了开发者的自定义做用域以后,开发者还须要让Spring容器可以识别发现这个新的做用域。下面的方法就是在Spring容器中用来注册新的做用域的。

void registerScope(String scopeName, Scope scope);

这个方法是在ConfigurableBeanFactory的接口中声明的,在大多数的ApplicationContext的实现中都是能够用的,能够经过BeanFactory属性来调用。

registerScope(..)方法的第一个参数是做用域相关联的惟一的一个名字;举例来讲,好比Spring容器之中的singletonprototype就是这样的名字。第二个参数就是咱们根据Scope接口所实现的具体的对象。

假定开发者实现了自定义的做用域,而后按照以下步骤来注册。

下面的例子使用了SimpleThreadScope,这个例子Spring中是有实现的,可是没有默认注册。开发者自实现的Scope也能够经过以下方式来注册。

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

以后,开发者能够经过以下相似的Bean定义来使用自定义的Scope:

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

在定制的Scope中,开发者也不限于仅仅经过编程方式来注册本身的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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://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="bar" class="x.y.Bar" scope="thread">
        <property name="name" value="Rick"/>
        <aop:scoped-proxy/>
    </bean>

    <bean id="foo" class="x.y.Foo">
        <property name="bar" ref="bar"/>
    </bean>

</beans>

至此,本文描述了关于Bean做用域的一些基本信息,在下一篇文章中,将会描述Bean的生命周期等信息。

相关文章
相关标签/搜索