在spring中,在xml中定義bean
時,scope
屬性是用來聲明bean
的做用域的。對於這個屬性,你也許已經很熟悉了,singleton
和prototype
信手捏來,甚至還能說出request
、session
、global session
,scope不就只有這麼幾個值嗎。java
emmm,話不要說太滿,容易打臉。常見的各種博客中,通常只會介紹上面說到的幾種可能值,但翻一翻官方的說明,你就會發現,事情並無這麼簡單。web
這是官方文檔中的介紹,scope屬性一共有六種可能值,驚不驚喜,意不意外。spring
下面,就讓咱們來一一看看各個值表明的意義。設計模式
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
下面作一個小實驗驗證一下: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>
testBean
的scope
爲singleton
,而變量bean
和bean1
所指向的實例都是從同一個IOC容器中獲取的,因此獲取的是同一個bean實例,所以分別對bean
和bean1
調用add方法後,num的值就會變成2。而bean2
是從另外一個IOC容器中獲取的,因此它是一個新的實例,num
的值便成了初始值0,調用add
方法後,num的值變成了1。這樣也驗證了上面所說的singleton
單例含義,指的是每個IOC容器中僅存在一個實例。
接下來是另外一個經常使用的scope:prototype
。與singleton
相反,設置爲prototype
的bean,每次調用容器的getBean
方法或注入到另外一個bean中時,都會返回一個新的實例。
與其餘的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
是什麼鬼?居然還有這樣的神仙scope??莫方,讓咱們來一探究竟。
這幾個類型的scope都只能在web環境下使用,若是使用 ClassPathXmlApplicationContext
來加載使用了該屬性的bean,那麼就會拋出異常。就像這樣:
java.lang.IllegalStateException: No Scope registered for scope name '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/>
這是個什麼東西???
這裏實際上是聲明對該bean使用代理模式,這樣作的話,容器在注入該bean的時候,將會使用CGLib動態代理
爲它建立一個代理對象,該對象擁有與原Bean相同的public接口並暴露,代理對象每次調用時,會從相應做用域範圍內(這裏是request
)獲取真正的TestBean
對象。
那麼,爲何要這樣作呢?
由於被注入的bean(testBean
)和目標bean(HelloController
)的生命週期不同,而同一個容器內的bean注入只會發生一次,你想一想,HelloController
是singleton
的,只會實例化一次,若是不使用代理對象,就意味着咱們只能將同一個request-bean
注入到這個singleton-bean
中,那以後的每次訪問,都將調用同一個testBean
實例,這不是咱們想要的結果。咱們但願HelloController
是容器範圍內單例的,同時想要一個做用域爲 Http Request
的testBean
實例,這時候,代理對象就扮演着不可或缺的角色了。
另外,值得一提的是,若是咱們對一個scope
爲prototype
的bean使用<aop:scoped-proxy/>
的話,那麼每次調用該bean的方法都會建立一個新的實例,關於這一點,你們能夠自行驗證。
代理方式默認是CGLib
,而且只有public
方法會被代理,private
方法是不會被代理的。若是咱們想要使用基於JDK
的代理來建立代理對象,那麼只須要將aop標籤中的proxy-target-class
屬性設置爲false便可,就像這樣:
<aop:scoped-proxy proxy-target-class="false"/>
但有個條件,那就是這個bean必需要實現某個接口。
咱們再來跑一下代碼驗證一下,啓動!
接下來訪問幾回http://127.0.0.1:8080/testBean
,輸出以下:
==========request start========== 0 1 1 2 ==========request end========== ==========request start========== 0 1 1 2 ==========request end==========
嗯,一切都在掌控範圍以內。
跟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
的做用域比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
中有效。
emmmm,說實話,這個驗證起來有點麻煩,摸索了半天沒有找到正確姿式,因此。。。。若是有知道如何驗證這一點的小夥伴歡迎留言補充。
也許你會發現,不少博客中說的 global session
怎麼不見了??
這你就不知道了吧,由於在最新版本(5.2.0.BUILD-SNAPSHOT)中global session
早就被移除了。
因此之後再有人問你,scope屬性有哪幾種可能值,分別表明什麼含義的時候,就能夠義正詞嚴的把這篇文章甩他臉上了。
關於 scope 的介紹到此就告一段落了,來作一個小結:
Http Request
。Http Session
。WebSocket
應用。但願這篇文章能對你有幫助,若是以爲還不錯的話,記得分享給身邊的小夥伴哦。
讓咱們紅塵做伴,活得瀟瀟灑灑。