BeanFactory除了拥有作为IoC Service Provider的职责,作为一个轻量级容器,它还有着其他一些职责,其中就包括对象的生命周期管理。
本节主要讲述容器中管理的对象的scope这个概念。多数中文资料在讲解bean的scope时喜欢用"作用域"这个名词,应该还算贴切吧。不过,我更希望告诉你scope这个词到底代表什么意思,至于你怎么称呼它反而不重要。
scope用来声明容器中的对象所应该处的限定场景或者说该对象的存活时间,即容器在对象进入其相应的scope之前,生成并装配这些对象,在该对象不再处于这些scope的限定之后,容器通常会销毁这些对象。打个比方吧!我们都是处于社会(容器)中,如果把中学教师作为一个类定义,那么当容器初始化这些类之后,中学教师只能局限在中学这样的场景中;中学,就可以看作中学教师的scope。
Spring容器最初提供了两种bean的scope类型:singleton和prototype,但发布2.0之后,又引入了另外三种scope类型,即request、session和global session类型。不过这三种类型有所限制,只能在Web应用中使用。也就是说,只有在支持Web应用的ApplicationContext中使用这三个scope才是合理的。
我们可以通过使用<bean>的singleton或者scope属性来指定相应对象的scope,其中,scope属性只能在XSD格式的文档声明中使用,类似于如下代码所演示的形式:
- DTD:
- <bean id="mockObject1" class="...MockBusinessObject" singleton="false"/>
- XSD:
- <bean id="mockObject2" class="...MockBusinessObject" scope="prototype"/>
让我们来看一下容器提供的这几个scope是如何限定相应对象的吧!
1. singleton
配置中的bean定义可以看作是一个模板,容器会根据这个模板来构造对象。但是要根据这个模板构造多少对象实例,又该让这些构造完的对象实例存活多久,则由容器根据bean定义的scope语意来决定。标记为拥有singleton scope的对象定义,在Spring的IoC容器中只存在一个实例,所有对该对象的引用将共享这个实例。该实例从容器启动,并因为第一次被请求而初始化之后,将一直存活到容器退出,也就是说,它与IoC容器"几乎"拥有相同的"寿命"。
图4-5是Spring参考文档中所给出的singleton的bean的实例化和注入语意演示图例,或许可以更形象地说明问题。
(点击查看大图)图4-5 singleton scope |
需要注意的一点是,不要因为名字的原因而与GoF 所提出的Singleton模式相混淆,二者的语意是不同的: 标记为singleton的bean是由容器来保证这种类型的bean在同一个容器中只存在一个共享实例; 而Singleton模式则是保证在同一个Classloader中只存在一个这种类型的实例。
可以从两个方面来看待singleton的bean所具有的特性。
对象实例数量。singleton类型的bean定义,在一个容器中只存在一个共享实例,所有对该类型bean的依赖都引用这一单一实例。这就好像每个幼儿园都会有一个滑梯一样,这个幼儿园的小朋友共同使用这一个滑梯。而对于该幼儿园容器来说,滑梯实际上就是一个singleton的bean。
对象存活时间。singleton类型bean定义,从容器启动,到它第一次被请求而实例化开始,只要容器不销毁或者退出,该类型bean的单一实例就会一直存活。
通常情况下,如果你不指定bean的scope,singleton便是容器默认的scope,所以,下面三种配置形式实际上达成的是同样的效果:
- <!-- DTD or XSD -->
- <bean id="mockObject1" class="...MockBusinessObject"/>
- <!-- DTD -->
- <bean id="mockObject1" class="...MockBusiness
Object" singleton="true"/> - <!-- XSD -->
- <bean id="mockObject1" class="...MockBusiness
Object" scope="singleton"/>
2. prototype
针对声明为拥有prototype scope的bean定义,容器在接到该类型对象的请求的时候,会每次都重新生成一个新的对象实例给请求方。虽然这种类型的对象的实例化以及属性设置等工作都是由容器负责的,但是只要准备完毕,并且对象实例返回给请求方之后,容器就不再拥有当前返回对象的引用,请求方需要自己负责当前返回对象的后继生命周期的管理工作,包括该对象的销毁。也就是说,容器每次返回给请求方一个新的对象实例之后,就任由这个对象实例"自生自灭"了。
让我们继续幼儿园的比喻,看看prototype在这里应该映射到哪些事物。儿歌里好像有句"排排坐,分果果",我们今天要分苹果咯!将苹果的bean定义的scope声明为prototype,在每个小朋友领取苹果的时候,我们都是分发一个新的苹果给他。发完之后,小朋友爱怎么吃怎么吃,爱什么时候吃什么时候吃。但是,吃完后要记得把果核扔到果皮箱哦! 而如果你把苹果的bean定义的scope声明为singleton会是什么情况呢?如果第一个小朋友比较谦让,那么他可能对这个苹果只咬一口,但是下一个小朋友吃多少就不知道了。当吃得只剩一个果核的时候,下一个来吃苹果的小朋友肯定要哭鼻子的。
所以,对于那些请求方不能共享使用的对象类型,应该将其bean定义的scope设置为prototype。这样,每个请求方可以得到自己对应的一个对象实例,而不会出现上面"哭鼻子"的现象。通常,声明为prototype的scope的bean定义类型,都是一些有状态的,比如保存每个顾客信息的对象。
从Spring 参考文档上的这幅图片(见图4-6),你可以再次了解一下拥有prototype scope的bean定义,在实例化对象并注入依赖的时候,它的具体语意是个什么样子。
(点击查看大图)图4-6 prototype scope |
- <!-- DTD -->
- <bean id="mockObject1"
class="...MockBusiness
Object"singleton="false"/> - <!-- XSD -->
- <bean id="mockObject1"
class="...MockBusiness
Object"scope="prototype"/>
4.3.5 bean的scope(2)
3. request、session和global session
这三个scope类型是Spirng 2.0之后新增加的,它们不像之前的singleton和prototype那么"通用",因为它们只适用于Web应用程序,通常是与XmlWebApplicationContext
注意 只能使用scope 属性才能指定这三种"bean的scope类型"。也就是说,你不得不使用基于XSD文档声明的XML配置文件格式。
request
request通常的配置形式如下:
- <bean id="requestProcessor" class="...
RequestProcessor" scope="request"/>
Spring容器,即XmlWebApplicationContext
session
对于Web应用来说,放到session中的最普遍的信息就是用户的登录信息,对于这种放到session中的信息,我们可使用如下形式指定其scope为session:
- <bean id="userPreferences" class="com.foo.
UserPreferences" scope="session"/>
Spring容器会为每个独立的session创建属于它们自己的全新的UserPreferences对象实例。与request相比,除了拥有session scope的bean的实例具有比request scope的bean可能更长的存活时间,其他方面真是没什么差别。
global session
还是userPreferences,不过scope对应的值换一下,如下所示:
- <bean id="userPreferences" class="com.foo.
UserPreferences" scope="globalSession"/>
global session只有应用在基于portlet的Web应用程序中才有意义,它映射到portlet的global范围的session。如果在普通的基于servlet的Web应用中使用了这个类型的scope,容器会将其作为普通的session类型的scope对待。
4. 自定义scope类型
在Spring 2.0之后的版本中,容器提供了对scope的扩展点,这样,你可以根据自己的需要或者应用的场景,来添加自定义的scope类型。需要说明的是,默认的singleton和prototype是硬编码到代码中的,而request、session和global session,包括自定义scope类型,则属于可扩展的scope行列,它们都实现了org.springframework.beans.factory.config.Scope接口,该接口定义如下:
- public interface Scope {
-
Object get(String name, ObjectFactory objectFactory); -
Object remove(String name); -
void registerDestructionCallb ack(String name,
Runnable callback); -
String getConversationId(); - }
要实现自己的scope类型,首先需要给出一个Scope接口的实现类,接口定义中的4个方法并非都是必须的,但get和remove方法必须实现。我们可以看一下http://www.jroller.com/eu/entry/implementing
代码清单4-28 自定义的ThreadScope的定义
- public class ThreadScope implements Scope {
-
private final ThreadLocal threadScope = new ThreadLocal() { -
protected Object initialValue() { -
return new HashMap(); -
} -
}; -
-
public Object get(String name, ObjectFactory objectFactory) { -
Map scope = (Map) threadScope.get(); -
Object object = scope.get(name); -
if(object==null) { -
object = objectFactory.getObject(); -
scope.put(name, object); -
} -
return object; -
} -
public Object remove(String name) { -
Map scope = (Map) threadScope.get(); -
return scope.remove(name); -
} -
public void registerDestructionCallb ack(String name,
Runnable callback) { -
} -
... - }
更多Scope相关的实例,可以参照同一站点的一篇文章"More fun with Spring scopes"(http://jroller. com/eu/entry/more_fun_with_spring_scopes),其中提到PageScope的实现。
有了Scope的实现类之后,我们需要把这个Scope注册到容器中,才能供相应的bean定义使用。通常情况下,我们可以使用ConfigurableBeanFactory的以下方法注册自定义scope:
- void registerScope(String scopeName, Scope scope);
其中,参数scopeName就是使用的bean定义可以指定的名称,比如Spring框架默认提供的自定义scope类型request或者session。参数scope即我们提供的Scope实现类实例。
对于以上的ThreadScope,如果容器为BeanFactory类型(当然,更应该实现Configurable- BeanFactory),我们可以通过如下方式来注册该Scope:
- Scope threadScope = new ThreadScope();
- beanFactory.registerScope("thread",threadScope);
之后,我们就可以在需要的bean定义中直接通过"thread"名称来指定该bean定义对应的scope为以上注册的ThreadScope了,如以下代码所示:
- <bean id="beanName" class="..." scope="thread"/>
除了直接编码调用ConfigurableBeanFactory的registerScope来注册scope,Spring还提供了一个专门用于统一注册自定义scope的BeanFactoryPostProcessor
代码清单4-29 使用CustomScopeConfigurer注册自定义scope
- <bean class="org.springframework.beans.factory.
config.CustomScopeConfigurer"> -
<property name="scopes"> -
<map> -
<entry key="thread" value="com.foo.ThreadScope"/> -
</map> -
</property> - </bean>
在以上工作全部完成之后,我们就可以在自己的bean定义中使用这个新增加到容器的自定义scope"thread"了,如下代码演示了通常情况下"thread"自定义scope的使用:
- <bean id="beanName" class="..." scope="thread">
-
<aop:scoped-proxy/> - </bean>
由于<aop:scoped-proxy/>涉及Spring AOP相关知识,这里不会详细讲述。需要注意的是,使用了自定义scope的bean定义,需要该元素来为其在合适的时间创建和销毁相应的代理对象实例。对于request、session和global session来说,也是如此。