scope 屬性說明
在spring中,在xml中定義bean
時,scope
屬性是用來聲明bean
的作用域的。對於這個屬性,你也許已經很熟悉了,singleton
和prototype
信手捏來,甚至還能說出request
、session
、global 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實例緩存起來,以供后續使用。
下面做一個小實驗驗證一下:
先寫一個測試類:
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容器中僅存在一個實例。
prototype
接下來是另一個常用的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 Scopes
request
和 session
這兩個你也許有所耳聞,但是 application
和 websocket
是什么鬼?竟然還有這樣的神仙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/>
這是個什么東西???
這里其實是聲明對該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==========
嗯,一切都在掌控范圍之內。
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屬性有哪幾種可能值,分別代表什么含義的時候,就可以理直氣壯的把這篇文章甩他臉上了。
總結
關於 scope 的介紹到此就告一段落了,來做一個小結:
- singleton:單例模式,每次獲取都返回同一個實例,相對於同一個IOC容器而言。
- prototype:原型模式,每次獲取返回不同實例,創建后的生命周期不再由IOC容器管理。
- request:作用域為同一個
Http Request
。 - session:作用域為同一個
Http Session
。 - application:作用域為同一個WEB容器,可以看做Web應用中的單例模式。
- websocket:作用域為同一個
WebSocket
應用。
希望這篇文章能對你有幫助,如果覺得還不錯的話,記得分享給身邊的小伙伴哦。
讓我們紅塵作伴,活得瀟瀟灑灑。