IoC容器5——bean作用域

原创
2017/06/23 13:53
阅读数 70

bean作用域

当创建bean的定义时,就创建了如何创建类实例的规则。bean定义是一个规则的思想很重要,因为这意味着可以从一个规则创建许多对象的实例,与类一样。

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

下面的作用域开箱即用。也可以创建用户自定义的作用域。

  • 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作用域。

下图阐述了Spring prototype 作用域。注意,一个dao一般不配置成prototype,因为一个典型的DAO不保持任何的会话状态;使用这幅图仅仅是因为可以重用singleton图的核心内容。

输入图片说明

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

<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()方法返回的对象。

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部