1. 程式人生 > 其它 >spring成神之路第六篇:玩轉bean scope,避免跳坑裡

spring成神之路第六篇:玩轉bean scope,避免跳坑裡

本文內容

  1. 詳細介紹5種bean的sope及使用注意點

  2. 自定義作用域的實現

應用中,有時候我們需要一個物件在整個應用中只有一個,有些物件希望每次使用的時候都重新建立一個,spring對我們這種需求也提供了支援,在spring中這個叫做bean的作用域,xml中定義bean的時候,可以通過scope屬性指定bean的作用域,如:

<beanid=""class=""scope="作用域"/>

spring容器中scope常見的有5種,下面我們分別來介紹一下。

singleton

當scope的值設定為singleton的時候,整個spring容器中只會存在一個bean例項,通過容器多次查詢bean的時候(呼叫BeanFactory的getBean方法或者bean之間注入依賴的bean物件的時候),返回的都是同一個bean物件,singleton是scope的預設值

,所以spring容器中預設建立的bean物件是單例的,通常spring容器在啟動的時候,會將scope為singleton的bean建立好放在容器中(有個特殊的情況,當bean的lazy被設定為true的時候,表示懶載入,那麼使用的時候才會建立),用的時候直接返回。

案例

bean xml配置
<!--單例bean,scope設定為singleton-->
<beanid="singletonBean"class="com.javacode2018.lesson001.demo4.BeanScopeModel"scope="singleton">
<constructor-arg
index="0"value="singleton"/>

</bean>
BeanScopeModel程式碼
packagecom.javacode2018.lesson001.demo4;

publicclassBeanScopeModel{
publicBeanScopeModel(StringbeanScope){
System.out.println(String.format("createBeanScopeModel,{sope=%s},{this=%s}",beanScope,this));
}
}

上面構造方法中輸出了一段文字,一會我們可以根據輸出來看一下這個bean什麼時候建立的,是從容器中獲取bean的時候建立的還是容器啟動的時候建立

的。

測試用例
packagecom.javacode2018.lesson001.demo4;

importorg.junit.Before;
importorg.junit.Test;
importorg.springframework.context.support.ClassPathXmlApplicationContext;

/**
*公眾號:路人甲Java,工作10年的前阿里P7分享Java、演算法、資料庫方面的技術乾貨!堅信用技術改變命運,讓家人過上更體面的生活!
*<p>
*bean作用域
*/

publicclassScopeTest{

ClassPathXmlApplicationContextcontext;

@Before
publicvoidbefore(){
System.out.println("spring容器準備啟動.....");
//1.bean配置檔案位置
StringbeanXml="classpath:/com/javacode2018/lesson001/demo4/beans.xml";
//2.建立ClassPathXmlApplicationContext容器,給容器指定需要載入的bean配置檔案
this.context=newClassPathXmlApplicationContext(beanXml);
System.out.println("spring容器啟動完畢!");
}

/**
*單例bean
*/

@Test
publicvoidsingletonBean(){
System.out.println("---------單例bean,每次獲取的bean例項都一樣---------");
System.out.println(context.getBean("singletonBean"));
System.out.println(context.getBean("singletonBean"));
System.out.println(context.getBean("singletonBean"));
}

}

上面程式碼中before方法上面有@Before註解,這個是junit提供的功能,這個方法會在所有@Test標註的方法之前之前執行,before方法中我們對容器進行初始化,並且在容器初始化前後輸出了一段文字。

上面程式碼中,singletonBean方法中,3次獲取singletonBean對應的bean。

執行測試用例
spring容器準備啟動.....
createBeanScopeModel,{sope=singleton},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@e874448}
spring容器啟動完畢!
---------單例bean,每次獲取的bean例項都一樣---------
com.javacode2018.lesson001.demo4.BeanScopeModel@e874448
com.javacode2018.lesson001.demo4.BeanScopeModel@e874448
com.javacode2018.lesson001.demo4.BeanScopeModel@e874448
結論

從輸出中得到2個結論

  • 前3行的輸出可以看出,BeanScopeModel的構造方法是在容器啟動過程中呼叫的,說明這個bean例項在容器啟動過程中就建立好了,放在容器中快取著

  • 最後3行輸出的是一樣的,說明返回的是同一個bean物件

單例bean使用注意

單例bean是整個應用共享的所以需要考慮到執行緒安全問題,之前在玩springmvc的時候,springmvc中controller預設是單例的,有些開發者在controller中建立了一些變數,那麼這些變數實際上就變成共享的了,controller可能會被很多執行緒同時訪問,這些執行緒併發去修改controller中的共享變數,可能會出現數據錯亂的問題;所以使用的時候需要特別注意。

prototype

如果scope被設定為prototype型別的了,表示這個bean是多例的,通過容器每次獲取的bean都是不同的例項,每次獲取都會重新建立一個bean例項物件

案例

bean xml配置
<!--多例bean,scope設定為prototype-->
<beanid="prototypeBean"class="com.javacode2018.lesson001.demo4.BeanScopeModel"scope="prototype">
<constructor-argindex="0"value="prototype"/>
</bean>
新增一個測試用例

ScopeTest中新增一個方法

/**
*多例bean
*/

@Test
publicvoidprototypeBean(){
System.out.println("---------多例bean,每次獲取的bean例項都不一樣---------");
System.out.println(context.getBean("prototypeBean"));
System.out.println(context.getBean("prototypeBean"));
System.out.println(context.getBean("prototypeBean"));
}
執行測試用例
spring容器準備啟動.....
spring容器啟動完畢!
---------多例bean,每次獲取的bean例項都不一樣---------
createBeanScopeModel,{sope=prototype},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@289d1c02}
com.javacode2018.lesson001.demo4.BeanScopeModel@289d1c02
createBeanScopeModel,{sope=prototype},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@22eeefeb}
com.javacode2018.lesson001.demo4.BeanScopeModel@22eeefeb
createBeanScopeModel,{sope=prototype},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@17d0685f}
com.javacode2018.lesson001.demo4.BeanScopeModel@17d0685f
結論

輸出中可以看出,容器啟動過程中並沒有去建立BeanScopeModel物件,3次獲取prototypeBean得到的都是不同的例項,每次獲取的時候才會去呼叫構造方法建立bean例項。

多例bean使用注意

多例bean每次獲取的時候都會重新建立,如果這個bean比較複雜,建立時間比較長會影響系統的效能,這個地方需要注意。

下面要介紹的3個:request、session、application都是在spring web容器環境中才會有的。

request

當一個bean的作用域為request表示在一次http請求中,一個bean對應一個例項;對每個http請求都會建立一個bean例項,request結束的時候,這個bean也就結束了,request作用域用在spring容器的web環境中,這個以後講springmvc的時候會說,spring中有個web容器介面WebApplicationContext,這個裡面對request作用域提供了支援,配置方式:

<beanid=""class=""scope="request"/>

session

這個和request類似,也是用在web環境中,session級別共享的bean,每個會話會對應一個bean例項,不同的session對應不同的bean例項,springmvc中我們再細說。

<beanid=""class=""scope="session"/>

application

全域性web應用級別的作用域,也是在web環境中使用的,一個web應用程式對應一個bean例項,通常情況下和singleton效果類似的,不過也有不一樣的地方,singleton是每個spring容器中只有一個bean例項,一般我們的程式只有一個spring容器,但是,一個應用程式中可以建立多個spring容器不同的容器中可以存在同名的bean,但是sope=aplication的時候,不管應用中有多少個spring容器,這個應用中同名的bean只有一個

<beanid=""class=""scope="application"/>

自定義scope

有時候,spring內建的幾種sope都無法滿足我們的需求的時候,我們可以自定義bean的作用域

自定義Scope 3步驟

第1步:實現Scope介面

我們來看一下這個介面定義

packageorg.springframework.beans.factory.config;

importorg.springframework.beans.factory.ObjectFactory;
importorg.springframework.lang.Nullable;

publicinterfaceScope{

/**
*返回當前作用域中name對應的bean物件
* name:需要檢索的bean的名稱
* objectFactory:如果name對應的bean在當前作用域中沒有找到,那麼可以呼叫這個ObjectFactory來建立這個物件
**/

Objectget(Stringname,ObjectFactory<?>objectFactory);

/**
*將name對應的bean從當前作用域中移除
**/

@Nullable
Objectremove(Stringname);

/**
*用於註冊銷燬回撥,如果想要銷燬相應的物件,則由Spring容器註冊相應的銷燬回撥,而由自定義作用域選擇是不是要銷燬相應的物件
*/

voidregisterDestructionCallback(Stringname,Runnablecallback);

/**
*用於解析相應的上下文資料,比如request作用域將返回request中的屬性。
*/

@Nullable
ObjectresolveContextualObject(Stringkey);

/**
*作用域的會話標識,比如session作用域將是sessionId
*/

@Nullable
StringgetConversationId();

}
第2步:將自定義的scope註冊到容器

需要呼叫org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope的方法,看一下這個方法的宣告

/**
*向容器中註冊自定義的Scope
*scopeName:作用域名稱
* scope:作用域物件
**/

voidregisterScope(StringscopeName,Scopescope);
第3步:使用自定義的作用域

定義bean的時候,指定bean的scope屬性為自定義的作用域名稱。

案例

需求

下面我們來實現一個執行緒級別的bean作用域,同一個執行緒中同名的bean是同一個例項不同的執行緒中的bean不同的例項

實現分析

需求中要求bean線上程中是共享的,所以我們可以通過ThreadLocal來實現,ThreadLocal可以實現執行緒中資料的共享。

下面我們來上程式碼。

ThreadScope
packagecom.javacode2018.lesson001.demo4;

importorg.springframework.beans.factory.ObjectFactory;
importorg.springframework.beans.factory.config.Scope;
importorg.springframework.lang.Nullable;

importjava.util.HashMap;
importjava.util.Map;
importjava.util.Objects;

/**
*自定義本地執行緒級別的bean作用域,不同的執行緒中對應的bean例項是不同的,同一個執行緒中同名的bean是同一個例項
*/

publicclassThreadScopeimplementsScope{

publicstaticfinalStringTHREAD_SCOPE="thread";//@1

privateThreadLocal<Map<String,Object>>beanMap=newThreadLocal(){
@Override
protectedObjectinitialValue(){
returnnewHashMap<>();
}
};

@Override
publicObjectget(Stringname,ObjectFactory<?>objectFactory){
Objectbean=beanMap.get().get(name);
if(Objects.isNull(bean)){
bean=objectFactory.getObject();
beanMap.get().put(name,bean);
}
returnbean;
}

@Nullable
@Override
publicObjectremove(Stringname){
returnthis.beanMap.get().remove(name);
}

@Override
publicvoidregisterDestructionCallback(Stringname,Runnablecallback){
//bean作用域範圍結束的時候呼叫的方法,用於bean清理
System.out.println(name);
}

@Nullable
@Override
publicObjectresolveContextualObject(Stringkey){
returnnull;
}

@Nullable
@Override
publicStringgetConversationId(){
returnThread.currentThread().getName();
}
}

@1:定義了作用域的名稱為一個常量thread,可以在定義bean的時候給scope使用

BeanScopeModel
packagecom.javacode2018.lesson001.demo4;

publicclassBeanScopeModel{
publicBeanScopeModel(StringbeanScope){
System.out.println(String.format("執行緒:%s,createBeanScopeModel,{sope=%s},{this=%s}",Thread.currentThread(),beanScope,this));
}
}

上面的構造方法中會輸出當前執行緒的資訊,到時候可以看到建立bean的執行緒。

bean配置檔案

beans-thread.xml內容

<?xmlversion="1.0"encoding="UTF-8"?>
<beansxmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd"
>


<!--自定義scope的bean-->
<beanid="threadBean"class="com.javacode2018.lesson001.demo4.BeanScopeModel"scope="thread">
<constructor-argindex="0"value="thread"/>
</bean>
</beans>

注意上面的scope是我們自定義的,值為thread

測試用例
packagecom.javacode2018.lesson001.demo4;

importorg.springframework.context.support.ClassPathXmlApplicationContext;

importjava.util.concurrent.TimeUnit;

/**
*公眾號:路人甲Java,工作10年的前阿里P7分享Java、演算法、資料庫方面的技術乾貨!堅信用技術改變命運,讓家人過上更體面的生活!
*<p>
*自定義scope
*/

publicclassThreadScopeTest{
publicstaticvoidmain(String[]args)throwsInterruptedException{
StringbeanXml="classpath:/com/javacode2018/lesson001/demo4/beans-thread.xml";
//手動建立容器
ClassPathXmlApplicationContextcontext=newClassPathXmlApplicationContext();
//設定配置檔案位置
context.setConfigLocation(beanXml);
//啟動容器
context.refresh();
//向容器中註冊自定義的scope
context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE,newThreadScope());//@1

//使用容器獲取bean
for(inti=0;i<2;i++){//@2
newThread(()->{
System.out.println(Thread.currentThread()+","+context.getBean("threadBean"));
System.out.println(Thread.currentThread()+","+context.getBean("threadBean"));
}).start();
TimeUnit.SECONDS.sleep(1);
}
}
}

注意上面程式碼,重點在@1,這個地方向容器中註冊了自定義的ThreadScope。

@2:建立了2個執行緒,然後在每個執行緒中去獲取同樣的bean 2次,然後輸出,我們來看一下效果。

執行輸出
執行緒:Thread[Thread-1,5,main],createBeanScopeModel,{sope=thread},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@4049d530}
Thread[Thread-1,5,main],com.javacode2018.lesson001.demo4.BeanScopeModel@4049d530
Thread[Thread-1,5,main],com.javacode2018.lesson001.demo4.BeanScopeModel@4049d530
執行緒:Thread[Thread-2,5,main],createBeanScopeModel,{sope=thread},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@87a76da}
Thread[Thread-2,5,main],com.javacode2018.lesson001.demo4.BeanScopeModel@87a76da
Thread[Thread-2,5,main],com.javacode2018.lesson001.demo4.BeanScopeModel@87a76da

從輸出中可以看到,bean在同樣的執行緒中獲取到的是同一個bean的例項不同的執行緒bean的例項是不同的。

總結

  1. spring容器自帶的有2種作用域,分別是singleton和prototype;還有3種分別是spring web容器環境中才支援的request、session、application

  2. singleton是spring容器預設的作用域,一個spring容器中同名的bean例項只有一個,多次獲取得到的是同一個bean;單例的bean需要考慮執行緒安全問題

  3. prototype是多例的,每次從容器中獲取同名的bean,都會重新建立一個;多例bean使用的時候需要考慮建立bean對效能的影響

  4. 一個應用中可以有多個spring容器

  5. 自定義scope 3個步驟,實現Scope介面,將實現類註冊到spring容器,使用自定義的sope

案例原始碼

連結:https://pan.baidu.com/s/1p6rcfKOeWQIVkuhVybzZmQ
提取碼:zr99

來源:https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ==&mid=2648933960&idx=1&sn=f4308f8955f87d75963c379c2a0241f4&chksm=88621e76bf159760d404c253fa6716d3ffce4de8df0fc1d0d5dd0cf00a81bc170a30829ee58f&token=1314297026&lang=zh_CN&scene=21#wechat_redirect