IoC容器5——bean做用域

bean做用域

当建立bean的定义时,就建立了如何建立类实例的规则。bean定义是一个规则的思想很重要,由于这意味着能够从一个规则建立许多对象的实例,与类同样。java

从一个特定的bean定义中建立的bean,不只能够控制它的各类依赖关系和配置值,还能够控制对象的做用域。这种方法是强大、灵活的,能够经过配置选择要建立对象的做用域,而不须要在Java类的层面。能够为Bean指定许多做用域中的一个,而且开箱即用,Spring Framework支持七个做用域,其中有五个只在基于web ApplicationContext中有效。web

下面的做用域开箱即用。也能够建立用户自定义的做用域。spring

  • singleton 默认的,每个Spring IoC容器都拥有惟一的一个实例对象。
  • prototype 一个bean定义能够有任何数量的对象实例。
  • request 一个bean定义的做用域适用于单个HTTP请求的生命周期;也就是说,每一个HTTP请求都有本身的bean定义实例。只有在Web ApplicationContext生效。
  • session 一个bean定义的做用域适用于HTTP Session的生命周期。只有在Web ApplicationContext生效。
  • globalSession 一个bean定义的做用域适用于全局的HTTP Session的生命周期。通常只有使用Porlet Context时才有效。只有在Web ApplicationContext生效。
  • application 一个bean定义的做用域适用于ServletContext的生命周期。只有在Web ApplicationContext生效。
  • websocket 一个bean定义的做用域适用于WebSocket的生命周期。只有在Web ApplicationContext生效。

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

1 Singleton

只管理一个共享的singleton bean 的实例;而且全部使用id的bean请求若是匹配到这个bean定义,Spring容器都返回同一个特定的bean的实例。设计模式

换句话说,当定义一个bean定义并将它的做用域设置为singleton,Spring IoC容器将仅建立一个由该bean定义指定的对象的实例。这个单独的实例存储在singleton bean的缓存中,对该命名bean的全部后续请求和引用都返回缓存的对象。缓存

输入图片说明

Spring的singleton bean的概念有别于GoF 设计模式书中的单例模式。GoF单例硬编码对象的做用域,使得每一个ClassLoader只建立一个特定类的实例。每一个容器一个bean是对Spring singleton做用域的最好描述。若是在单个Spring容器中为指定的类定义一个bean,则Spring容器将建立由bean定义指定的类的惟一一个实例。Singleton做用域是Spring bean的默认做用域。使用XML定义一个singleton 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"/>

2 Prototype

使用prototype 的bean部署方式会在每次请求一个特定bean的时候都建立一个bean的实例。对bean的每次请求意味着将bean注入到其它bean中或者经过调用容器的getBean()方法请求它。应该对有状态的bean使用prototype做用域,对无状态的bean使用singleton做用域。websocket

下图阐述了Spring prototype 做用域。注意,一个dao通常不配置成prototype,由于一个典型的DAO不保持任何的会话状态;使用这幅图仅仅是由于能够重用singleton图的核心内容。session

输入图片说明

下面的例子在XML中将一个bean定义为prototype:app

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

相对于其它做用域,Spring无论理prototype bean 的整个生命周期:容器实例化、配置并装配一个prototype对象,最后将其交给客户,此后再也不记录prototype实例。所以,尽管全部做用域的bean的initialization生命周期的回掉函数都会被调用,可是在prototype状况下,配置的destruction生命周期回掉函数不会被调用。客户端代码必须本身清楚prototype对象并释放它持有的昂贵资源。为了让Spring容器释放prototype bean的资源,可使用自定义的bean post-processor,它会持有须要清理的bean的引用。

在某些方面,Spring 规则认为prototype bean是Java new 操做法的替代。全部new以后的生命周期都必须由客户端管理。

3 Singleton bean 拥有 prototype bean 依赖

当你使用有prototype bean依赖的singleton bean时,要注意依赖关系是在实例化时被解析。所以若是你依赖注入一个prototype bean到singleton bean中,一个新的prototype bean 会被实例化而且被依赖注入singleton bean。提供给singleton bean的prototype的实例是惟一的。

可是,假设你想在运行时让singleton bean每次都获得一个prototype bean的新实例。你就不能将prototype bean依赖注入到singleton bean中,由于注入仅仅发生一次,是在Spring容器实例化singleton bean并解析和注入依赖关系时。若是想要在运行时获取prototype bean的新实例不止一次,可使用“方法注入”。

4 Request,session,global session,application 和 WebSocket 做用域

Request,session,global session,application 和 WebSocket 做用域仅在使用基于web的Spring应用上下文(例如XmlWebApplicationContext)中可用。若是在普通的Spring IoC容器(例如ClassPathXmlApplicationContext)中使用这些做用域,会抛出IllegalStateException异常来讲明未知的bean做用域。

初始化 web 配置

为支持Request,session,global session,application 和 WebSocket 做用域,在定义bean以前须要一些基础的初始化配置。(对于标准的做用域,singleton和prototype,不须要这些初始化配置)。

如何进行初始化设置取决于不一样的Servlet环境。

若是你使用Spring Web MVC来得到做用域中的bean,实际上就是使用Spring的DispatcherServlet或者DispatcherPortlet来处理一个请求,那么不须要特别的配置:DispatcherServlet和DispatcherPortlet已经暴露了全部相关的状态。

若是使用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>

若是配置listener时出现问题,可使用Spring的RequestContextFilter。过滤器的映射取决于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>

DispatcherServlet、RequestContextListener和RequestContextFilter都作了相同的事情,即绑定Http请求对象处处理请求的线程。这使得request和session做用域的bean在跟进一步的调用链上可用。

Request 做用域

对于下面的XML格式的一个bean定义:

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

Spring容器使用loginAction bean 定义为每一个HTTP 请求建立一个LoginAction bean的实例。即loginAction bean 的做用域被设置为Http request级别。能够为所欲为的改变实例的内部状态,由于从相同的loginAction bean 定义建立的其它实例对这个状态的修改不可见;它们特定于单个请求。当请求处理完成,request做用域的bean就失效了。

当使用注解的方式配置,@RequestScope注解用来指定request做用域。

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

Session 做用域

对于下面的XML格式的一个bean定义:

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

Spring容器使用userPreferences bean定义为每一个HTTP Session生命周期建立一个UserPreferences bean的实例。换言之,userPreference bean的做用域被设置为Http session级别。能够为所欲为的改变实例的内部状态,由于从相同的userPreference bean 定义建立的其它实例对这个状态的修改不可见;它们特定于每一个会话。当会话结束,session做用域的bean就失效了。

当使用注解的方式配置,@SessionScope注解用来指定session做用域。

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

Global session 做用域

对于下面的XML格式的一个bean定义:

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

globalSession做用域与标准的HTTP session做用域类似,而且仅应用于基于portlet的web应用的上下文。portlet文档定义了global session的概念——在全部组成一个portlet web应用的所用portlet中共享的会话。globalSession bean的做用域与global portlet session的生命周期一致。

若是是在标准的基于Servlet的web应用程序中定义了globalSession做用域,会使用标准的Http session做用域,不会发生错误。

Application 做用域

对于下面的XML格式的一个bean定义:

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

Spring容器在整个web应用中使用appPreferences bean定义仅仅建立一次AppPreferences的新实例。即appPreferences bean的做用域是ServletContext级别的,它被保存为ServletContext属性。它与Spring singleton bean在某种程度上类似,但有如下两点重要的不一样:它对于每一个ServletContext是单例,而不是每一个ApplicationContext(一个web应用中可能有多个ApplicationContext);它被暴露为一个ServletContext属性。

当使用注解的方式配置,@ApplicationScope注解用来指定application做用域。

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

做用域bean的依赖

Spring IoC容器不只管理对象的实例化,还组织它们之间的协做(或者依赖)。若是你想注入一个HTTP request做用域的bean到另一个更长生命周期的bean,也许得选择注入一个AOP代理来替代做用域bean。也就是说,须要注入一个代理对象来暴露与做用域bean相同的公有接口,能够在相关的做用域(例如HTTP request)取回真正的目标对象而且在实际的对象上调用代理方法。

能够在singleton bean之间使用aop:scoped-proxy/,经过可序列化的中间代理的引用,能够在从新得到目标单例bean时将其反序列化。

当声明了一个指向prototype做用域的aop:scoped-proxy/时,共享代理的每一次方法调用都会建立新的目标bean.

做用域代理并非获取做用域较小的bean惟一的生命周期安全的途径。也能够简单的声明注入点(即构造函数/setter方法的参数或者自动装配的字段)为ObjectFactory<MyTargetBean>,它容许在每次须要的时候调用getObject()方法获取如今的实例,而不用持用实例或单独保存它。

JSR-330将其称为Provider,使用Provider<MyTargetBean>声明和相关的get()方法调用。

下面的例子中仅有一行配置,可是去理解背后的原理很重要:

<?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/来建立一个代理。为何做用域为request 、session、globalSession和用户自定义做用域的bean须要aop:scoped-proxy/元素?能够分析接下来的单例定义并与上述定义作比较(注意,下面的userPreferences bean定义时不完整的)。

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

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

上面的例子中,单例bean userManager有一个Http session做用域的bean userPreferences的引用被注入。突出的问题是userManager bean是一个单例:每一个容器只会实例化一次,而且它的依赖关系(在这里只有一个userPreferences bean)也只被注入一次。这意味着userManager bean仅仅只会操做一个相同的userPreferences对象,即最开始注入的那个。

这并非将一个短生命周期bean注入到长生命周期bean所须要的行为,例如将一个HTTP session做用域的bean注入到singleton bean中。而 你须要一个userManager对象的单例、每一个session须要一个userPreference对象。所以容器建立了一个对象用来暴露UserPreferences类的相同公有接口,能够经过这个对象来获取做用域(HTTP request,Session等)机制上的实际UserPreferences对象。容器将这个代理对象注入userManager bean,并不知道这是UserPreferences 引用的一个代理。在这个例子中,当UserManager实例调用被依赖注入的UserPreferences对象的方法时,它其实是调用了代理的方法。而后代理获取HTTP Session(在这个例子中)上的实际的UserPreferences对象,而且代理了实际UserPreferences对象的方法调用。

所以,当注入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>

选择要建立的代理类型

默认状况下,当使用aop:scoped-proxy/元素来为bean建立一个代理,使用的是基于CGLIB的类代理。

CGLIB代理仅仅拦截公有方法调用。在此代理商不要待用非公有方法;这不会委派给实际的做用域目标对象。

做为替代,能够配置Spring容器为做用域bean建立标准JDK的基于接口的代理,将aop:scoped-proxy/标签的proxy-target-class属性声明为false。使用JDK基于接口的代理不须要向应用的classpath添加额外的库。可是,这也要求做用域bean的类必须至少实现一个接口,而且所用被注入到做用域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>

5 用户自定义做用域

bean做用域机制是可扩展的;能够定义本身的做用域,或者重定义现有的做用域,尽管重定义被认为是一种很差的实践而且不能覆盖内建的singleton和prototype做用域。

建立用户自定义做用域

为了集成自定义做用域到Spring容器,须要实现org.springframework.beans.factory.config.Scope接口。如何实现自定义的做用域,能够查看Spring Frameworkd自身提供的Scope实现和Scope javadocs,它更详细的解释了须要实现的方法。

Scope接口有四个方法用来从做用域获取对象、从做用域删除对象,和容许它们被销毁。

下面的方法从底层做用域返回对象。例如,会话做用域的实现返回session做用域的bean(若是不存在,方法在绑定bean的新实例到session以后返回它)。

Object get(String name, ObjectFactory objectFactory)

下面的方法从底层做用域移除对象。方法须要返回对象,可是当指定名称的对象不存在时能够返回null。

Object remove(String name)

下面的方法注册当scope被销毁时或scope中的指定对象被销毁时,scope须要执行的回掉函数。

void registerDestructionCallback(String name, Runnable destructionCallback)

下面的方法获取底层做用域的会话标识符。不一样做用域的标识符不一样。对于会话做用域的实现,这个标识符能够是会话的标识符。

String getConversationId()

使用用户自定义做用域

在测试几个用户自定义做用域实现后,须要让Spring容器了解你的新做用域。下面的方法是注册新做用域的核心方法:

void registerScope(String scopeName, Scope scope);

这个方法在ConfigurableBeanFactory接口中声明,在大多数的具体ApplicationContext实现上可用。

registerScope(...)方法的第一个参数是与做用域相关的惟一名称;Spring容器自身的名称例子是singleton和prototype。第二个参数是一个要注册和使用的Scope实现的实例。

假设实现一个Scope,能够用以下方法定义:

下面的例子中使用的SimpleThreadScope包含在Spring中,可是默认未被注册。这个注册方法对用户自定义的Scope实现相同。

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

而后就能够建立遵循自定义范围规则的bean定义:

<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>

当在FactoryBean实现中放置aop:scoped-proxy/元素,它做用域工厂bean自身,而不是它的getObject()方法返回的对象。

相关文章
相关标签/搜索