bean的作用域
在创建一个bean定义(通常为XML配置文件)时,你可以简单的将其理解为:用以创建由该bean定义所决定的实际对象实例的一张“处方(recipe)”或者模板。就如class一样,根据一张“处方”你可以创建多个对象实例。
你不仅可以控制注入到对象(bean定义)中的各种依赖和配置值,还可以控制该对象的作用域。这样你可以灵活选择所建对象的作用域,而不必在 Java Class级定义作用域。Spring Framework支持五种作用域(其中有三种只能用在基于web的Spring ApplicationContext
)。
内置支持的作用域分列如下:
表 3.4. Bean作用域
作用域 |
描述 |
singleton
|
在每个Spring IoC容器中一个bean定义对应一个对象实例。
|
prototype
|
一个bean定义对应多个对象实例。
|
request
|
在一次HTTP请求中,一个bean定义对应一个实例;即每次HTTP请求将会有各自的bean实例,它们依据某个bean定义创建而成。该作用域仅在基于web的Spring ApplicationContext 情形下有效。
|
session
|
在一个HTTP Session 中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext 情形下有效。
|
global session
|
在一个全局的HTTP Session 中,一个bean定义对应一个实例。典型情况下,仅在使用portlet context的时候有效。该作用域仅在基于web的Spring ApplicationContext 情形下有效。
|
当一个bean的作用域为singleton, 那么Spring IoC容器中只会存在一个共享的bean实例,并且所有对bean的请求,只要id与该bean定义相匹配,则只会返回bean的同一实例。
换言之,当把一个bean定义设置为singlton作用域时,Spring IoC容器只会创建该bean定义的唯一实例。这个单一实例会被存储到单例缓存(singleton cache)中,并且所有针对该bean的后续请求和引用都将返回被缓存的对象实例。
下图演示了Spring的singleton作用域。
请注意Spring的singleton bean概念与“四人帮”(GoF)模式一书中定义的Singleton模式是完全不同的。经典的GoF Singleton模式中所谓的对象范围是指在每一个ClassLoader
中指定class创建的实例有且仅有一个。把Spring的singleton作用域描述成一个container
对应一个bean实例最为贴切。亦即,假如在单个Spring容器内定义了某个指定class的bean,那么Spring容器将会创建一个且仅有一个由该bean定义指定的类实例。
Singleton作用域是Spring中的缺省作用域。要在XML中将bean定义成singleton,可以这样配置:
<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"/>
<!-- the following is equivalent, though redundant (and preserved for backward compatibility) -->
<bean id="accountService" class="com.foo.DefaultAccountService" singleton="true"/>
Prototype作用域的bean会导致在每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()
方法)时都会创建一个新的bean实例。根据经验,对所有有状态的bean应该使用prototype作用域,而对无状态的bean则应该使用singleton作用域。
下图演示了Spring的prototype作用域。请注意,典型情况下,DAO不会被配置成prototype,因为一个典型的DAO不会持有任何会话状态,因此应该使用singleton作用域。
要在XML中将bean定义成prototype,可以这样配置:
<bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/>
<!-- the following is equivalent too (and preserved for backward compatibility) -->
<bean id="accountService" class="com.foo.DefaultAccountService" singleton="false"/>
对于prototype作用域的bean,有一点非常重要,那就是Spring不能对一个prototype bean的整个生命周期负责:容器在初始化、配置、装饰或者是装配完一个prototype实例后,将它交给客户端,随后就对该prototype实例不 闻不问了。不管何种作用域,容器都会调用所有对象的初始化生命周期回调方法,而对prototype而言,任何配置好的析构生命周期回调方法都将不会被 调用。清除prototype作用域的对象并释放任何prototype bean所持有的昂贵资源,都是客户端代码的职责。(让Spring容器释放被singleton作用域bean占用资源的一种可行方式是,通过使用 bean的后置处理器,该处理器持有要被清除的bean的引用。)
谈及prototype作用域的bean时,在某些方面你可以将Spring容器的角色看作是Java new
操作符的替代者。任何迟于该时间点的生命周期事宜都得交由客户端来处理。在第 3.5.1 节 “Lifecycle接口”一节中会进一步讲述Spring IoC容器中的bean生命周期。
向后兼容性:在XML中指定生命周期作用域
如果你在bean定义文件中引用'spring-beans.dtd'
DTD,要显式说明bean的生命周期作用域你必须使用"singleton
"属性(记住singleton生命周期作用域是默认的)。 如果引用的是'spring-beans-2.0.dtd'
DTD或者是Spring 2.0 XSD schema,那么需要使用"scope
"属性(因为"singleton
"属性被删除了,新的DTD和XSD文件使用"scope
"属性)。
简单地说,如果你用"singleton
"属性那么就必须在那个文件里引用'spring-beans.dtd'
DTD。 如果你用"scope
"属性那么必须 在那个文件里引用'spring-beans-2.0.dtd'
DTD 或'spring-beans-2.0.xsd'
XSD。
其他作用域,即request
、session
以及global session
仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架)。
注意
下面介绍的作用域仅仅在使用基于web的Spring ApplicationContext
实现(如XmlWebApplicationContext
)时有用。如果在普通的Spring IoC容器中,比如像XmlBeanFactory
或ClassPathXmlApplicationContext
,尝试使用这些作用域,你将会得到一个IllegalStateException
异常(未知的bean作用域)。
要使用request
、session
和 global session
作用域的bean(即具有web作用域的bean),在开始设置bean定义之前,还要做少量的初始配置。请注意,假如你只想要“常规的”作用域,也就是singleton和prototype,就不需要这一额外的设置。
在目前的情况下,根据你的特定servlet环境,有多种方法来完成这一初始设置。如果你使用的是Servlet 2.4及以上的web容器,那么你仅需要在web应用的XML声明文件web.xml
中增加下述ContextListener
即可
<web-app>
...
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
...
</web-app>
如果你用的是早期版本的web容器(Servlet 2.4以前),那么你要使用一个javax.servlet.Filter
的实现。请看下面的web.xml配置片段:
<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>
RequestContextListener
和RequestContextFilter
两个类做的都是同样的工作:将HTTP request对象绑定到为该请求提供服务的Thread
。这使得具有request和session作用域的bean能够在后面的调用链中被访问到。
考虑下面bean定义:
<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>
针对每次HTTP请求,Spring容器会根据loginAction
bean定义创建一个全新的LoginAction
bean实例,且该loginAction
bean实例仅在当前HTTP request内有效,因此可以根据需要放心的更改所建实例的内部状态,而其他请求中根据loginAction
bean定义创建的实例,将不会看到这些特定于某个请求的状态变化。当处理请求结束,request作用域的bean实例将被销毁。
考虑下面bean定义:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
针对某个HTTP Session
,Spring容器会根据userPreferences
bean定义创建一个全新的userPreferences
bean实例,且该userPreferences
bean仅在当前HTTP Session
内有效。与request作用域
一样,你可以根据需要放心的更改所创建实例的内部状态,而别的HTTP Session
中根据userPreferences
创建的实例,将不会看到这些特定于某个HTTP Session
的状态变化。当HTTP Session
最终被废弃的时候,在该HTTP Session
作用域内的bean也会被废弃掉。
3.4.3.4. global session作用域
考虑下面bean定义:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="globalSession"/>
global session
作用域类似于标准的HTTP Session
作用域,不过它仅仅在基于portlet的web应用中才有意义。Portlet规范定义了全局Session
的概念,它被所有构成某个portlet web应用的各种不同的portlet所共享。在global session
作用域中定义的bean被限定于全局portlet Session
的生命周期范围内。
请注意,假如你在编写一个标准的基于Servlet的web应用,并且定义了一个或多个具有global session
作用域的bean,系统会使用标准的HTTP Session
作用域,并且不会引起任何错误。
能够在HTTP request或者Session
(甚至自定义) 作用域中定义bean固然很好,但是Spring IoC容器除了管理对象(bean)的实例化,同时还负责协作者(或者叫依赖)的实例化。如果你打算将一个Http request范围的bean注入到另一个bean中,那么需要注入一个AOP代理来替代被注入的作用域bean。也就是说,你需要注入一个代理对象,该 对象具有与被代理对象一样的公共接口,而容器则可以足够智能的从相关作用域中(比如一个HTTP request)获取到真实的目标对象,并把方法调用委派给实际的对象。
注意
<aop:scoped-proxy/>
不能和作用域为singleton
或prototype
的bean一起使用。为singleton bean创建一个scoped proxy将抛出BeanCreationException
异常。
让我们看一下将相关作用域bean作为依赖的配置,配置并不复杂(只有一行),但是理解“为何这么做”以及“如何做”是很重要的。
<?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-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<!-- a HTTP Session
-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
<!-- this next element effects the proxying of 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>
在XML配置文件中,要创建一个作用域bean的代理,只需要在作用域bean定义里插入一个<aop:scoped-proxy/>
子元素即可(你可能还需要在classpath里包含CGLIB库,这样容器就能够实现基于class的代理;还可能要使用基于XSD的配置)。上述XML配置展示了“如何做”,现在讨论“为何这么做”。在作用域为request
、session
以及globalSession
的bean定义里,为什么需要这个<aop:scoped-proxy/>
元素呢?下面我们从去掉<aop:scoped-proxy/>
元素的XML配置开始说起:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
<bean id="userManager" class="com.foo.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
从上述配置中可以很明显的看到singleton bean userManager
被注入了一个指向HTTP Session
作用域bean userPreferences
的引用。singleton userManager
bean会被容器仅实例化一次,并且其依赖(userPreferences
bean)也仅被注入一次。这意味着,userManager
在理论上只会操作同一个userPreferences
对象,即原先被注入的那个bean。而注入一个HTTP Session
作用域的bean作为依赖,有违我们的初衷。因为我们想要的只是一个userManager
对象,在它进入一个HTTP Session
生命周期时,我们希望去使用一个HTTP Session
的userPreferences
对象。
当注入某种类型对象时,该对象实现了和UserPreferences
类一样的公共接口(即UserPreferences
实例)。并且不论我们底层选择了何种作用域机制(HTTP request、Session
等等),容器都会足够智能的获取到真正的
UserPreferences
对象,因此我们需要将该对象的代理注入到userManager
bean中, 而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>
在Spring 2.0中,Spring的bean作用域机制是可以扩展的。这意味着,你不仅可以使用Spring提供的预定义bean作用域; 还可以定义自己的作用域,甚至重新定义现有的作用域(不提倡这么做,而且你不能覆盖内置的singleton
和prototype
作用域)。
作用域由接口org.springframework.beans.factory.config.Scope
定义。要将你自己的自定义作用域集成到Spring容器中,需要实现该接口。它本身非常简单,只有两个方法,分别用于底层存储机制获取和删除对象。自定义作用域可能超出了本参考手册的讨论范围,但你可以参考一下Spring提供的Scope
实现,以便于去如何着手编写自己的Scope实现。
在实现一个或多个自定义Scope
并测试通过之后,接下来就是如何让Spring容器识别你的新作用域。ConfigurableBeanFactory
接口声明了给Spring容器注册新Scope
的主要方法。(大部分随Spring一起发布的BeanFactory
具体实现类都实现了该接口);该接口的主要方法如下所示:
void registerScope(String scopeName, Scope scope);
registerScope(..)
方法的第一个参数是与作用域相关的全局唯一名称;Spring容器中该名称的范例有singleton
和prototype
。registerScope(..)
方法的第二个参数是你打算注册和使用的自定义Scope
实现的一个实例。
假设你已经写好了自己的自定义Scope
实现,并且已经将其进行了注册:
// note: the ThreadScope
class does not exist; I made it up for the sake of this example
Scope customScope = new ThreadScope();
beanFactory.registerScope("thread", scope);
然后你就可以像下面这样创建与自定义Scope
的作用域规则相吻合的bean定义了:
<bean id="..." class="..." scope="thread"/>
如果你有自己的自定义Scope
实现,你不仅可以采用编程的方式注册自定义作用域,还可以使用BeanFactoryPostProcessor
实现:CustomScopeConfigurer
类,以声明的方式注册Scope
。BeanFactoryPostProcessor
接口是扩展Spring IoC容器的基本方法之一,在本章的BeanFactoryPostProcessor中将会介绍。
使用CustomScopeConfigurer
,以声明方式注册自定义Scope
的方法如下所示:
<?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-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread" value="com.foo.ThreadScope"/>
</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>
CustomScopeConfigurer
既允许你指定实际的Class
实例作为entry的值,也可以指定实际的Scope
实现类实例;详情请参见CustomScopeConfigurer
类的JavaDoc