基本概念

Scope,也称作用域,在 Spring IoC 容器是指其创建的 Bean 对象相对于其他 Bean 对象的请求可见范围

在 Spring IoC 容器中具有以下几种作用域:基本作用域(request、prototype),Web 作用域(reqeust、session、globalsession),自定义作用域。

Spring 的作用域在装配 Bean 时就必须在配置文件中指明,配置方式如下(以 xml 配置文件为例):

<!-- 具体的作用域需要在 scope 属性中定义 -->
<bean id="animals" class="com.demo.Animals" scope="xxx" />

基本作用域

1.singleton

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 容器中有且只有一个实例。


2.prototype

prototype,也称原型作用域。每次向 Spring IoC 容器请求获取 Bean 都返回一个全新的Bean。相对于 singleton 来说就是不缓存 Bean,每次都是一个根据 Bean 定义创建的全新 Bean。

  • 在配置文件中定义:
<bean id="animals" class="com.demo.Animals" scope="prototype" />
  • 调用验证,参照 singleton 的例子,观察输出结果,发现每次调用返回不同的实例。

Web 作用域

1.reqeust

request,表示每个请求需要容器创建一个全新Bean。

在 Spring IoC 容器,即XmlWebApplicationContext 会为每个 HTTP 请求创建一个全新的 RequestPrecessor 对象。当请求结束后,该对象的生命周期即告结束。当同时有 10 个 HTTP 请求进来的时候,容器会分别针对这 10 个请求创建 10 个全新的 RequestPrecessor 实例,且他们相互之间互不干扰,从不是很严格的意义上说,request 可以看做 prototype 的一种特例,除了场景更加具体之外,语意上差不多。

  • 在配置文件中定义:
<bean id="animals" class="com.demo.Animals" scope="request" />
  • 调用验证(这里以 SpringMVC 为例,模拟了不同的两个请求)
@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。


2.session

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 实例。


3.globalSession

globalSession,类似于session 作用域,只是其用于 portlet 环境的 web 应用。如果在非portlet 环境将视为 session 作用域。


自定义作用域

1.实例探究

自定义作用域,需要实现 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();
}

接下来看看如何使用自定义的作用域

  • 自定义作用域 ThreadScope,表示 Bean 作用范围为同一个线程:
public class ThreadScope implements Scope {

    // 用于存放线程中的 Bean
    private final ThreadLocal<Map<String, Object>> THREAD_SCOPE = new ThreadLocal<Map<String, Object>>() {
        protected Map<String, Object> initialValue() {
            return new HashMap<String, Object>();
        }
    };

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Map<String, Object> map = THREAD_SCOPE.get();

        if (!map.containsKey(name)) {
            map.put(name, objectFactory.getObject());
        }

        return map.get(name);
    }

    @Override
    public Object remove(String name) {
        Map<String, Object> map = THREAD_SCOPE.get();
        return map.remove(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
    }

    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Override
    public String getConversationId() {
        return null;
    }

}
  • 在配置文件定义:
<!-- CustomScopeConfigurer 的 scopes 属性注册自定义作用域实现 -->
<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 返回的是不同实例。


2.原理探究

这里以自定义作用域为例,探究下 scope 的基本原理。在 Spring 中对于 Bean 的 scope(作用域)的检查发生在【获取 Bean】的过程中。获取方法如下:

Animals animals1 = (Animals) factory.getBean("animals");

由此可见,获取 Bean 的入口在 BeanFacotry 中定义。而 BeanFacotry 的基本功能实现都在它的基本实现类 AbstractBeanFactory 中,具体的调用过程这里不再探究,简单的调用流程如下: getBean -> doGetBean。因此这里重点来看下 doGetBean 这个方法:

protected <T> T doGetBean(final String name, final Class<T> 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<Object>() {

                        // 若不存在该标识的 bean,则触发 map.put(name, objectFactory.getObject()) 的 getObject 方法
                        @Override
                        public Object getObject() throws BeansException {


                            beforePrototypeCreation(beanName);

                            try {
                                // 创建 Bean 实例 
                                return createBean(beanName, mbd, args);
                            } finally {
                                afterPrototypeCreation(beanName);
                            }
                        }

                    });
                    bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);

                } catch (IllegalStateException ex) {
                    // 抛出异常...
                }
            }

        } catch (BeansException ex) {
            cleanupAfterBeanCreationFailure(beanName);
            throw ex;
        }
    }

    // 省略部分源码...
}

参考

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐