【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

scope 属性说明

在spring中,在xml中定义bean时,scope属性是用来声明bean的做用域的。对于这个属性,你也许已经很熟悉了,singletonprototype信手捏来,甚至还能说出requestsessionglobal session,scope不就只有这么几个值吗。java

emmm,话不要说太满,容易打脸。常见的各种博客中,通常只会介绍上面说到的几种可能值,但翻一翻官方的说明,你就会发现,事情并无这么简单。web

这是官方文档中的介绍,scope属性一共有六种可能值,惊不惊喜,意不意外。spring

下面,就让咱们来一一看看各个值表明的意义。设计模式

singleton

singleton是scope属性的默认值,当咱们把bean的scope属性设置为singleton时,表明将对该bean使用单例模式,单例想必你们都熟悉,也就是说每次使用该bean的id从容器中获取该bean的时候,都将会返回同一个bean实例。但这里的单例跟设计模式里的单例还有一些小区别。浏览器

设计模式中的单例是经过硬编码,给某个类仅建立一个静态对象,而且只暴露一个接口来获取这个对象实例,所以,设计模式中的单例是相对ClassLoader而言的,同一个类加载器下只会有一个实例。缓存

下面就是经典的使用double-check实现的懒加载代码:springboot

public class Singleton{
    private static volatile Singleton FRANK;

    public static Singleton getInstance(){
        if (FRANK == null){
            synchronized(this){
                if (FRANK == null) FRANK = new Singleton();
            }
        }
        return FRANK;
    }
}

可是在Spring中,singleton单例指的是每次从同一个IOC容器中返回同一个bean对象,单例的有效范围是IOC容器,而不是ClassLoader。IOC容器会将这个bean实例缓存起来,以供后续使用。websocket

20190307095059.png

下面作一个小实验验证一下:session

先写一个测试类:app

public class TestScope {
    @Test
    public void testSingleton(){
        ApplicationContext context = new ClassPathXmlApplicationContext("test-bean.xml");
        TestBean bean = (TestBean) context.getBean("testBean");
        Assert.assertEquals(bean.getNum() , 0);
        bean.add();
        Assert.assertEquals(bean.getNum() , 1);

        TestBean bean1 = (TestBean) context.getBean("testBean");
        Assert.assertEquals(bean1.getNum() , 1);
        bean1.add();
        Assert.assertEquals(bean1.getNum() , 2);

        ApplicationContext context1 = new ClassPathXmlApplicationContext("test-bean.xml");
        TestBean bean2 = (TestBean) context1.getBean("testBean");
        Assert.assertEquals(bean2.getNum() , 0);
        bean2.add();
        Assert.assertEquals(bean2.getNum() , 1);
    }
}
public class TestBean {
    private int num;

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public void add(){
        num++;
    }
}

这是相应的配置文件test-bean.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">

<beans>
    <bean id="testBean" class="com.frank.spring.bean.scope.TestBean" scope="singleton"/>
</beans>

testBeanscopesingleton,而变量beanbean1所指向的实例都是从同一个IOC容器中获取的,因此获取的是同一个bean实例,所以分别对beanbean1调用add方法后,num的值就会变成2。而bean2是从另外一个IOC容器中获取的,因此它是一个新的实例,num的值便成了初始值0,调用add方法后,num的值变成了1。这样也验证了上面所说的singleton单例含义,指的是每个IOC容器中仅存在一个实例。

prototype

接下来是另外一个经常使用的scope:prototype。与singleton相反,设置为prototype的bean,每次调用容器的getBean方法或注入到另外一个bean中时,都会返回一个新的实例。

20190307191454.png

与其余的scope类型不一样的是,Spring并不会管理设置为prototype的bean的整个生命周期,获取相关bean时,容器会实例化,或者装配相关的prototype-bean实例,而后返回给客户端,但不会保存prototype-bean的实例。因此,尽管全部的bean对象都会调用配置的初始化方法,可是prototype-bean并不会调用其配置的destroy方法。因此清理工做必须由客户端进行。因此,Spring容器对prototype-bean 的管理在必定程度上相似于 new 操做,对象建立后的事情将所有由客户端处理。

仍旧用一个小栗子来进行测试:

咱们将上面的xml文件进行修改:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">

<beans>
    <bean id="testBean" class="com.frank.spring.bean.scope.TestBean" scope="prototype"/>
</beans>
@Test
public void testPrototype(){
    ApplicationContext context = new ClassPathXmlApplicationContext("test-bean.xml");
    TestBean bean = (TestBean) context.getBean("testBean");
    Assert.assertEquals(bean.getNum() , 0);
    bean.add();
    Assert.assertEquals(bean.getNum() , 1);

    TestBean bean1 = (TestBean) context.getBean("testBean");
    Assert.assertEquals(bean1.getNum() , 0);
    bean1.add();
    Assert.assertEquals(bean1.getNum() , 1);
}

这里两次从同一个IOC容器中获取testBean,获得了两个不一样的bean实例,这就是prototype的做用。

接着,咱们配置一个初始化方法和销毁方法,来测试一下:

给TestBean类加两个方法:

public class TestBean {
    private int num;

    public void init(){
        System.out.println("init TestBean");
    }

    public void destroy(){
        System.out.println("destroy TestBean");
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public void add(){
        num++;
    }
}

而后在配置文件里设置它的初始化方法和销毁方法:

<beans>
    <bean id="testBean" class="com.frank.spring.bean.scope.TestBean" scope="prototype"  init-method="init"  destroy-method="destroy"/>
</beans>

仍是用以前的测试方法:

@Test
public void testPrototype(){
    ApplicationContext context = new ClassPathXmlApplicationContext("test-bean.xml");
    TestBean bean = (TestBean) context.getBean("testBean");
    Assert.assertEquals(bean.getNum() , 0);
    bean.add();
    Assert.assertEquals(bean.getNum() , 1);

    TestBean bean1 = (TestBean) context.getBean("testBean");
    Assert.assertEquals(bean1.getNum() , 0);
    bean1.add();
    Assert.assertEquals(bean1.getNum() , 1);
}

输出以下:

init TestBean
init TestBean

能够看到,仅仅输出了初始化方法init中的内容,而没有输出销毁方法destroy中的内容,因此,对于prototype-bean而言,在xml中配置destroy-method属性是没有意义的,容器在建立这个bean实例后就抛弃它了,若是它持有的资源须要释放,则须要客户端进行手动释放才行。这大概就是亲生和领养的区别吧。

另外,若是将一个prototype-bean注入到一个singleton-bean中,那么每次从容器中获取的singleton-bean对应prototype-bean都是同一个,由于依赖注入仅会进行一次。

Request && Session && Application && WebSocket Scopes

requestsession 这两个你也许有所耳闻,可是 applicationwebsocket 是什么鬼?居然还有这样的神仙scope??莫方,让咱们来一探究竟。

这几个类型的scope都只能在web环境下使用,若是使用 ClassPathXmlApplicationContext 来加载使用了该属性的bean,那么就会抛出异常。就像这样:

java.lang.IllegalStateException: No Scope registered for scope name 'request'

下面让咱们依次来看看这几个值的做用。

request

若是将scope属性设置为 request 表明该bean的做用域为单个请求,请求结束,则bean将被销毁,第二次请求将会建立一个新的bean实例,让咱们来验证一下。方便起见,建立一个springboot应用,而后建立一个配置类并指定其扫描的xml:

@Configuration
@ImportResource(locations = {"classpath:application-bean.xml"})
public class WebConfiguration {
}

如下是xml中的内容:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="testBean" class="com.frank.springboothello.model.TestBean" scope="request" >
        <aop:scoped-proxy/>
    </bean>
</beans>

下面是controller的内容:

@RestController
public class HelloController {

    @Autowired
    private TestBean testBean;

    @Autowired
    private TestBean testBean1;

    @GetMapping("/testBean")
    public void testBean(){
        System.out.println("==========request start==========");
        System.out.println(testBean.getNum());
        testBean.add();
        System.out.println(testBean.getNum());
        System.out.println(testBean1.getNum());
        testBean1.add();
        System.out.println(testBean1.getNum());
        System.out.println("==========request end==========");
    }
}

这里仍是使用以前的TestBean,也许细心的你会发现,这里有一个乱入的家伙:

<aop:scoped-proxy/>

20190308093050.png

这是个什么东西???

这里实际上是声明对该bean使用代理模式,这样作的话,容器在注入该bean的时候,将会使用CGLib动态代理为它建立一个代理对象,该对象拥有与原Bean相同的public接口并暴露,代理对象每次调用时,会从相应做用域范围内(这里是request)获取真正的TestBean对象。

那么,为何要这样作呢?

由于被注入的bean(testBean)和目标bean(HelloController)的生命周期不同,而同一个容器内的bean注入只会发生一次,你想一想,HelloControllersingleton的,只会实例化一次,若是不使用代理对象,就意味着咱们只能将同一个request-bean注入到这个singleton-bean中,那以后的每次访问,都将调用同一个testBean实例,这不是咱们想要的结果。咱们但愿HelloController是容器范围内单例的,同时想要一个做用域为 Http RequesttestBean实例,这时候,代理对象就扮演着不可或缺的角色了。

另外,值得一提的是,若是咱们对一个scopeprototype的bean使用<aop:scoped-proxy/>的话,那么每次调用该bean的方法都会建立一个新的实例,关于这一点,你们能够自行验证。

代理方式默认是CGLib,而且只有public方法会被代理,private方法是不会被代理的。若是咱们想要使用基于JDK的代理来建立代理对象,那么只须要将aop标签中的proxy-target-class属性设置为false便可,就像这样:

<aop:scoped-proxy proxy-target-class="false"/>

但有个条件,那就是这个bean必需要实现某个接口。

咱们再来跑一下代码验证一下,启动!

20190308092733.png

接下来访问几回http://127.0.0.1:8080/testBean,输出以下:

==========request start==========
0
1
1
2
==========request end==========
==========request start==========
0
1
1
2
==========request end==========

嗯,一切都在掌控范围以内。

session

request相似,但它的生命周期更长一些,是在同一次会话范围内有效,也就是说若是不关闭浏览器,无论刷新多少次,都会访问同一个bean。

咱们将上面的xml稍做改动:

<bean id="testBean" class="com.frank.springboothello.model.TestBean" scope="session" >
    <aop:scoped-proxy/>
</bean>

再也运行一下,而后在页面刷新几回:

==========request start==========
0
1
1
2
==========request end==========
==========request start==========
2
3
3
4
==========request end==========
==========request start==========
4
5
5
6
==========request end==========

能够看到,num的值一直的增长,可见咱们访问的是同一个bean实例。

而后,咱们使用另外一个浏览器继续访问该页面:

==========request start==========
0
1
1
2
==========request end==========
==========request start==========
2
3
3
4
==========request end==========

发现num又从0开始计数了。这样就验证了咱们对session做用域的想法。

application

application的做用域比session又要更广一些,session做用域是针对一个 Http Session,而application做用域,则是针对一个 ServletContext ,有点相似 singleton,可是singleton表明的是每一个IOC容器中仅有一个实例,而同一个web应用中,是可能会有多个IOC容器的,但一个Web应用只会有一个 ServletContext,因此 application 才是web应用中货真价实的单例模式。

来测试一下,继续修改上面的xml文件:

<bean id="testBean" class="com.frank.springboothello.model.TestBean" scope="application" >
    <aop:scoped-proxy/>
</bean>

而后再次启动后,疯狂访问。

==========request start==========
0
1
1
2
==========request end==========
==========request start==========
2
3
3
4
==========request end==========
==========request start==========
4
5
5
6
==========request end==========

换个浏览器继续访问:

==========request start==========
6
7
7
8
==========request end==========
==========request start==========
8
9
9
10
==========request end==========

嗯,验证完毕。

websocket

websocket 的做用范围是 WebSocket ,即在整个 WebSocket 中有效。

emmmm,说实话,这个验证起来有点麻烦,摸索了半天没有找到正确姿式,因此。。。。若是有知道如何验证这一点的小伙伴欢迎留言补充。

global session

也许你会发现,不少博客中说的 global session 怎么不见了??

这你就不知道了吧,由于在最新版本(5.2.0.BUILD-SNAPSHOT)中global session早就被移除了。

因此之后再有人问你,scope属性有哪几种可能值,分别表明什么含义的时候,就能够义正词严的把这篇文章甩他脸上了。

20190308191406.png

总结

关于 scope 的介绍到此就告一段落了,来作一个小结:

  1. singleton:单例模式,每次获取都返回同一个实例,相对于同一个IOC容器而言。
  2. prototype:原型模式,每次获取返回不一样实例,建立后的生命周期再也不由IOC容器管理。
  3. request:做用域为同一个 Http Request
  4. session:做用域为同一个 Http Session
  5. application:做用域为同一个WEB容器,能够看作Web应用中的单例模式。
  6. websocket:做用域为同一个WebSocket应用。

但愿这篇文章能对你有帮助,若是以为还不错的话,记得分享给身边的小伙伴哦。

让咱们红尘做伴,活得潇潇洒洒。

相关文章
相关标签/搜索