当我们提供一一份bean的定义时,我们实际制作了如何创建一个bean的菜谱,这意味着,基于这个定义我们可以创建任意多个bean实例。Bean作用域的概念,定义了bean实例的有效范围,Spring支持6种作用域范围,其中4种只在web应用中有效。
作用域 | 描述 |
---|---|
singleton | 单例作用域,spring内置,在整个容器内有效 |
prototype | 原型作用域,spring内置,可以创建任意实例数量,任何时候向容器请求该bean,都会返回一个新的实例 |
request | 请求作用域,每个Http请求使用一个独立的bean实例,在web-aware容器中有效 |
session | 会话作用域,每个http会话使用一个独立的bean实例,在web-aware容器中有效 |
application | 在每个ServeletContext的生命周期使用一个独立bean的实例,web-aware容器中有效 |
websocket | 在每个WebSocket的生命周期内使用一个独立bean的实例,web-aware容器中有效 |
SingleTon作用域
SingleTon作用域的bean在每个容器范围内只会有一个实例,一旦被创建就会被容器缓存,所有的对该bean的请求,都会返回这个缓存的实例。SingleTon是默认的作用域,所以下面两行定义,效果是一样的。
<bean id="accountService" class="com.something.DefaultAccountService"/>
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
这里的单例和我们平时学习的设计模式里面的“单例模式”有本质上的区别,Spring的单例作用域,指每个容器一个实例,如果有多个容器,或者你强行用new操作符来创建实例,那在JVM内就会有多个实例。
Prototype作用域
prototype作用域的bean在每次请求的时候,都会创建一个新的实例。“请求“发生在是指依赖注入时,或直接调用容器的getBean()方法的时候。
ProtoType作用域一般用于有状态的bean对象,参照以下定义:
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
与其他作用域不一样的是,对于prototype作用域的bean,容器并不会对其生命周期完全负责。具体说,容器在完成bean的创建,初始化,装配之后,就返回给请求方,然后就不会再管它了;因此初始化相关的回调方法会被调用,而销毁相关的回调方法不会被调用。
因此我们可以理解:对于prototype的bean来说,容器仅仅实现一个更强大的工厂模式,容器不管理其生命周期。
singleton的bean依赖Prototype的bean
如果一个singleton作用域的bean A通过属性依赖于一个prototype作用域的bean B,由于依赖注入只会执行一次,那么在A的生命周期内,该属性都指向同一个实例B。如果你希望每次使用这个属性时候,都创建一个新的B实例,就要使用上一节讲的Method Injection技术了。
Request, Session, Application 和 WebSocket作用域
这些作用域在web-aware的ApplicationContext中才有效,比如XmlWebApplicationContext;否则容器初始化的过程中会抛出IllegalStateException。
要支持这些作用域,容器初始化的时候需要有一些额外的配置,spring web mvc里面,是通过DispatcherServlet完成的。当Http请求发生时,DispatcherServlet将request对象绑定到当前的服务线程,后续调用链上对request和session作用域的bean的访问会依赖这个对象。
定义一个reqeust作用域的bean方式如下:
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
其他类型作用域分别将scope属性值改成session,application,websocket即可。
这些非SingleTon的scope相当于一系列扩展的小容器,暂存了对应作用域bean的实例引用,Context不对这些bean的生命周期负责,只是将对这些bean的访问转发到对应的scope实现。
示例代码的scope_web子工程展示了request作用域的效果。
不同生命周期作用域bean的相互依赖
如果一个生命周期更长(比如singleton)的bean,依赖一个生命周期更短的bean,我们怎么办?前面讲过,依赖注入只在bean初始化时发生一次,所以无论我们通过属性,还是通过构造参数,都无法达到目的。
为了解决这个问题,spring允许我们注入bean一个特定类型的Proxy,所有对这个bean的调用先发生在proxy上,proxy再去当前的作用域定位目标bean,并转发这次调用。
下面的定义,告诉容器,为这个session作用域的bean包裹一层proxy。
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<!-- instructs the container to proxy the surrounding bean -->
<aop:scoped-proxy/>
</bean>
除此之外,还可以使用ObjectFactory来包装依赖注入点的bean类型引用:
public class SimpleMovieLister {
private ObjectFactory<MovieFinder> movieFinder;
@Inject
public SimpleMovieLister(ObjectFactory<MovieFinder> movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.get().findMovies(...);
...
}
}
自定义作用域
我们可以创建自定义的作用域,或者修改现有作用域的行为(内置作用域singleTon和prototype除外)。
实现自定义作用域需要实现org.springframework.beans.factory.config.Scope
接口,这接口有四个方法:
方法名 | 描述 |
---|---|
Object get(String name, ObjectFactory objectFactory) | 从scope返回一个bean,scope的实现者按照自己的规则来返回缓存的bean,或者调用objectFactory创建新的bean |
void registerDestructionCallback(String name, Runnable destructionCallback) | 注册一个callback,scope应当保存此callback,当scope要删除一个bean时,调用此callBack,确保对象能够被正确destroy |
Object remove(String name) | 从scope移除一个bean,此方法由spring来调用,具体不清楚何种情况下会被调用 |
String getConversationId() | 为scope实例提供一个唯一的ID,这个ID的含义和用法取决于具体实现,比如SessionScope使用session id,不需要的话可返回null |
我们调用容器的void registerScope(String scopeName, Scope scope)
方法来注册Scope。
结合scope的实现原理和Spring源码,很容易搞明白request scope的工作机制:
- 容器一看bean定义的scope是request,启动时不会创建它;
- 当发生对bean的请求时,依据scope名字找到对应的scope对象,调用scope的get方法;
- request scope背后为当前request建立了一个缓存结构(这个结构是
RequestContextHolder.currentRequestAttributes()
),缓存了所有request scoped bean; - scope查找该存储结构里是否有缓存的bean,没有就通过objectFactory创建并缓存起来;
- 当request销毁时,同时删除该存储结构里所有的bean。
示例代码
示例代码的scope_xml子工程,做了一个简单的示例,实现了一个叫做minute的Scope,意为”1分钟作用域“。
MinuteScope的实现如下:
public class MinuteScope implements Scope {
private Map<String, Object> objectMap = new ConcurrentHashMap<>();
private Map<String, Integer> objectMinute = new ConcurrentHashMap<>();
private Map<String,Runnable> destructionCallBack = new ConcurrentHashMap<>();
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Date date = new Date(System.currentTimeMillis());
if (objectMinute.containsKey(name) && objectMinute.get(name) != date.getMinutes()) {
destructionCallBack.get(name).run();
}
if (!objectMap.containsKey(name)) {
objectMap.put(name, objectFactory.getObject());
objectMinute.put(name, date.getMinutes());
}
return objectMap.get(name);
}
@Override
public Object remove(String name) {
destructionCallBack.remove(name);
return objectMap.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
destructionCallBack.put(name,callback);
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
Date date = new Date(System.currentTimeMillis());
return date.getMinutes()+"";
}
}
- get方法,Spring容器调用该方法获取所需的bean,参数一是bean的名字,参数二是bean工厂方法的封装;scope的实现去查找有没有合适的bean缓存,如果有就返回,否则调用工厂方法创建一个; 同时,如果发现有过期的bean缓存,执行销毁逻辑。
- objectFactory创建bean的时候,会调用scope的registerDestructionCallback,注册一个销毁bean的回调函数,当scope准备销毁一个bean的时候,应当调用此回调函数,以执行一个完整的bean销毁流程(见下一节);
- remove方法,主可调用此方法来删除一个bean,这里的约定是调用者,负责执行destroy逻辑;
- ConversationId,实际上容器并不会使用该参数,scope自己可以利用该值,来进行内部缓存的索引,不需要的话可以返回null;
- resolveContextualObject,上下文对象,返回scope关联的上下文,不需要则可以返回null;
4和5相当于Scope定义留下的两个约定,不关乎scope的本质,可以暂时忽略。
scope总结
通过上面实现自定义scope的尝试,我们可以洞察到两个关键点:
1、在Spring容器内部,bean的缓存和bean的创建是分开的,因此在scope的实现中,可以借助objectFactory来创建bean;
2、Spring可以有多个bean缓存结构,内置有一个SingleTon缓存结构,Scope相当于额外的缓存结构;Spring容器仍然完全负责bean创建和初始化,只是把bean的生命周期和作用域管理委托给了对应的scope实现。