Scope,也称作用域,在 Spring IoC 容器是指其创建的 Bean 对象相对于其他 Bean 对象的请求可见范围。
在 Spring IoC 容器中具有以下几种作用域:基本作用域(request、prototype),Web 作用域(reqeust、session、globalsession),自定义作用域。
Spring 的作用域在装配 Bean 时就必须在配置文件中指明,配置方式如下(以 xml 配置文件为例):
<bean id="animals" class="com.demo.Animals" scope="xxx" />
singleton,也称单例作用域。在每个 Spring IoC 容器中有且只有一个实例,而且其完整生命周期完全由 Spring 容器管理。对于所有获取该 Bean 的操作 Spring 容器将只返回同一个 Bean。
需要注意的是,若一个 Bean 未指定 scope 属性,默认也为 singleton 。
<bean id="animals" class="com.demo.Animals" scope="singleton" />
String location = ...
ApplicationContext factory = new FileSystemXmlApplicationContext(location);
// 获取 Bean
Animals animals = (Animals) factory.getBean("animals");
Animals animals2 = (Animals) factory.getBean("animals");
System.out.println(animals);
System.out.println(animals2);
//输出结果:
//com.demo.Animals@2151b0a5
//com.demo.Animals@2151b0a5
观察输出结果,发现多次获取 Bean,返回的都是同一个 Bean,再次验证了其在 Spring IoC 容器中有且只有一个实例。
prototype,也称原型作用域。每次向 Spring IoC 容器请求获取 Bean 都返回一个全新的Bean。相对于 singleton 来说就是不缓存 Bean,每次都是一个根据 Bean 定义创建的全新 Bean。
<bean id="animals" class="com.demo.Animals" scope="prototype" />
request,表示每个请求需要容器创建一个全新Bean。
在 Spring IoC 容器,即XmlWebApplicationContext 会为每个 HTTP 请求创建一个全新的 RequestPrecessor 对象。当请求结束后,该对象的生命周期即告结束。当同时有 10 个 HTTP 请求进来的时候,容器会分别针对这 10 个请求创建 10 个全新的 RequestPrecessor 实例,且他们相互之间互不干扰,从不是很严格的意义上说,request 可以看做 prototype 的一种特例,除了场景更加具体之外,语意上差不多。
<bean id="animals" class="com.demo.Animals" scope="request" />
@RequestMapping(value = "/hello1")
public void hello1(HttpServletRequest request) throws Exception {
// 在 Web 程序取得 Ioc 容器
ApplicationContext context = (ApplicationContext) WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
Animals animals1 = (Animals) context.getBean(Animals.class);
animals1.setName("animals");
Animals animals2 = (Animals) context.getBean(Animals.class);
System.out.println(animals1.getName());
System.out.println(animals2.getName());
// 输出结果:
// animals
// animals
}
@RequestMapping(value = "/hello2")
public void hello2(HttpServletRequest request) throws Exception {
ApplicationContext context = (ApplicationContext) WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
Animals animals = (Animals) context.getBean(Animals.class);
System.out.println(animals1.getName());
// 输出结果:
// null
}
观察输出结果,发现在同一个请求中(hello1 )多次获取 Bean 返回的是同一个实例,而在不同请求(hello2)中获取 Bean 返回的是不同的 Bean。
session,表示每个会话需要容器创建一个全新 Bean。比如对于每个用户一般会有一个会话,该用户的用户信息需要存储到会话中,此时可以将该 Bean 配置为 web 作用域。
<bean id="animals" class="com.demo.Animals" scope="session" />
@RequestMapping(value = "/hello1")
public void hello1(HttpServletRequest request) throws Exception {
// 在 Web 程序取得 Ioc 容器
ApplicationContext context = (ApplicationContext) WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
Animals animals1 = (Animals) context.getBean(Animals.class);
animals1.setName("animals");
Animals animals2 = (Animals) context.getBean(Animals.class);
System.out.println(animals1);
System.out.println(animals2);
}
观察输出结果,会发现不同浏览器(不同会话)返回的 Bean 实例不同,而同一个浏览器(同一会话)多次发起请求返回的是同一个 Bean 实例。
globalSession,类似于session 作用域,只是其用于 portlet 环境的 web 应用。如果在非portlet 环境将视为 session 作用域。
自定义作用域,需要实现 Scope 接口。
首先来看该接口的定义:
public interface Scope {
// 从作用域中获取Bean, objectFactory 表示当在当前作用域没找到合适Bean时使用它创建一个新的Bean
Object get(String name, ObjectFactory> objectFactory);
// 从作用域中移除 Bean
Object remove(String name);
// 用于注册销毁回调,如果想要销毁相应的对象则由Spring容器注册相应的销毁回调,而由自定义作用域选择是不是要销毁相应的对象;
void registerDestructionCallback(String name, Runnable callback);
// 用于解析相应的上下文数据,比如request作用域将返回request中的属性。
Object resolveContextualObject(String key);
// 作用域的会话标识,比如session作用域将是sessionId。
String getConversationId();
}
接下来看看如何使用自定义的作用域
public class ThreadScope implements Scope {
// 用于存放线程中的 Bean
private final ThreadLocal
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread" >
<bean class="scope.ThreadScope"/>
entry>
map>
property>
bean>
<bean id="animals" class="com.demo.Animals" scope="thread"/>
public static void main(String [ ] args) {
String location = "WebRoot/WEB-INF/spring-bean.xml";
ApplicationContext factory = new FileSystemXmlApplicationContext(location);
// 在 main 线程获取 Bean
Animals animals1 = (Animals) factory.getBean("animals");
Animals animals2 = (Animals) factory.getBean("animals");
System.out.println(animals1);
System.out.println(animals2);
// 新建线程获取 Bean
Thread thread = new Thread() {
public void run() {
String location = "WebRoot/WEB-INF/spring-bean.xml";
ApplicationContext factory = new FileSystemXmlApplicationContext(location);
Animals animals = (Animals) factory.getBean("animals");
System.out.println(animals);
}
};
thread.start();
// 输出结果:
// com.demo.Animals@3ddfd90f
// com.demo.Animals@3ddfd90f
// com.demo.Animals@153b2cb
}
观察输出结果,发现同一线程多次获取 Bean 返回的是同一个实例,而不同线程获取 Bean 返回的是不同实例。
这里以自定义作用域为例,探究下 scope 的基本原理。在 Spring 中对于 Bean 的 scope(作用域)的检查发生在【获取 Bean】的过程中。获取方法如下:
Animals animals1 = (Animals) factory.getBean("animals");
由此可见,获取 Bean 的入口在 BeanFacotry 中定义。而 BeanFacotry 的基本功能实现都在它的基本实现类 AbstractBeanFactory 中,具体的调用过程这里不再探究,简单的调用流程如下: getBean -> doGetBean。因此这里重点来看下 doGetBean 这个方法:
protected T doGetBean(final String name, final Class requiredType, final Object [ ] args, boolean typeCheckOnly)
throws BeansException {
final String beanName = transformedBeanName(name);
// 省略部分源码...
try {
// 省略部分源码...
// 判断 scope 作用域
// 若既不是 singleton 也不是 prototype,表明该 Bean 的作用域是自定义作用域或 web 作用域
if (mbd.isSingleton()) {
// 省略部分源码...
}else if (mbd.isPrototype()) {
// 省略部分源码...
}else {
// 取得 Bean 的 scope 名称,这里指 thread
String scopeName = mbd.getScope();
// 取得在 CustomScopeConfigurer 中定义的 scope 对象,这里指 ThreadScope 对象
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
// 抛出异常...
}
try {
// 调用 scope 的 get 方法
Object scopedInstance = scope.get(beanName, new ObjectFactory