【Spring源碼解讀】bean標簽中的屬性(一)你可能還不夠了解的 scope 屬性


scope 屬性說明

在spring中,在xml中定義bean時,scope屬性是用來聲明bean的作用域的。對於這個屬性,你也許已經很熟悉了,singletonprototype信手捏來,甚至還能說出requestsessionglobal session,scope不就只有這么幾個值嗎。

emmm,話不要說太滿,容易打臉。常見的各類博客中,一般只會介紹上面說到的幾種可能值,但翻一翻官方的說明,你就會發現,事情並沒有這么簡單。

這是官方文檔中的介紹,scope屬性一共有六種可能值,驚不驚喜,意不意外。

下面,就讓我們來一一看看各個值代表的意義。

singleton

singleton是scope屬性的默認值,當我們把bean的scope屬性設置為singleton時,代表將對該bean使用單例模式,單例想必大家都熟悉,也就是說每次使用該bean的id從容器中獲取該bean的時候,都將會返回同一個bean實例。但這里的單例跟設計模式里的單例還有一些小區別。

設計模式中的單例是通過硬編碼,給某個類僅創建一個靜態對象,並且只暴露一個接口來獲取這個對象實例,因此,設計模式中的單例是相對ClassLoader而言的,同一個類加載器下只會有一個實例。

下面就是經典的使用double-check實現的懶加載代碼:

public class Singleton{
    private static volatile Singleton FRANK;

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

但是在Spring中,singleton單例指的是每次從同一個IOC容器中返回同一個bean對象,單例的有效范圍是IOC容器,而不是ClassLoader。IOC容器會將這個bean實例緩存起來,以供后續使用。

20190307095059.png

下面做一個小實驗驗證一下:

先寫一個測試類:

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

希望這篇文章能對你有幫助,如果覺得還不錯的話,記得分享給身邊的小伙伴哦。

讓我們紅塵作伴,活得瀟瀟灑灑。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM