【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應用。

但願這篇文章能對你有幫助,若是以爲還不錯的話,記得分享給身邊的小夥伴哦。

讓咱們紅塵做伴,活得瀟瀟灑灑。

相關文章
相關標籤/搜索