Spring基础三:BeanScope

当我们提供一一份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的工作机制:

  1. 容器一看bean定义的scope是request,启动时不会创建它;
  2. 当发生对bean的请求时,依据scope名字找到对应的scope对象,调用scope的get方法;
  3. request scope背后为当前request建立了一个缓存结构(这个结构是RequestContextHolder.currentRequestAttributes()),缓存了所有request scoped bean;
  4. scope查找该存储结构里是否有缓存的bean,没有就通过objectFactory创建并缓存起来;
  5. 当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()+"";
    }
}
  1. get方法,Spring容器调用该方法获取所需的bean,参数一是bean的名字,参数二是bean工厂方法的封装;scope的实现去查找有没有合适的bean缓存,如果有就返回,否则调用工厂方法创建一个; 同时,如果发现有过期的bean缓存,执行销毁逻辑。
  2. objectFactory创建bean的时候,会调用scope的registerDestructionCallback,注册一个销毁bean的回调函数,当scope准备销毁一个bean的时候,应当调用此回调函数,以执行一个完整的bean销毁流程(见下一节);
  3. remove方法,主可调用此方法来删除一个bean,这里的约定是调用者,负责执行destroy逻辑;
  4. ConversationId,实际上容器并不会使用该参数,scope自己可以利用该值,来进行内部缓存的索引,不需要的话可以返回null;
  5. 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实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值