1. 程式人生 > >【第三章】 DI 之 3.4 Bean的作用域 ——跟我學spring3

【第三章】 DI 之 3.4 Bean的作用域 ——跟我學spring3

3.4  Bean的作用域

       什麼是作用域呢?即“scope”,在面向物件程式設計中一般指物件或變數之間的可見範圍。而在Spring容器中是指其建立的Bean物件相對於其他Bean物件的請求可見範圍。

Spring提供“singleton”和“prototype”兩種基本作用域,另外提供“request”、“session”、“global session”三種web作用域;Spring還允許使用者定製自己的作用域。

3.4.1  基本的作用域

       一、singleton:指“singleton”作用域的Bean只會在每個Spring IoC容器中存在一個例項,而且其完整生命週期完全由Spring容器管理。對於所有獲取該Bean的操作Spring容器將只返回同一個Bean。

GoF單例設計模式指“保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點”,介紹了兩種實現:通過在類上定義靜態屬性保持該例項和通過登錄檔方式。

1)通過在類上定義靜態屬性保持該例項:一般指一個Java虛擬機器 ClassLoader裝載的類只有一個例項,一般通過類靜態屬性保持該例項,這樣就造成需要單例的類都需要按照單例設計模式進行編碼;Spring沒采用這種方式,因為該方式屬於侵入式設計;程式碼樣例如下:

java程式碼:
  1. package cn.javass.spring.chapter3.bean;  
  2. publicclass Singleton {  
  3.     //1.私有化構造器
  4.     private Singleton() {}  
  5.     //2.單例快取者,惰性初始化,第一次使用時初始化
  6.     privatestaticclass InstanceHolder {  
  7.         privatestaticfinal Singleton INSTANCE = new Singleton();  
  8.     }  
  9.     //3.提供全域性訪問點
  10.     publicstatic Singleton getInstance() {  
  11.         return InstanceHolder.INSTANCE;  
  12.     }  
  13.     //4.提供一個計數器來驗證一個ClassLoader一個例項
  14.     privateint counter=0;  
  15. }  

以上定義個了個單例類,首先要私有化類構造器;其次使用InstanceHolder靜態內部類持有單例物件,這樣可以得到惰性初始化好處;最後提供全域性訪問點getInstance,使得需要該單例例項的物件能獲取到;我們在此還提供了一個counter計數器來驗證一個ClassLoader一個例項。具體一個ClassLoader有一個單例例項測試請參考程式碼“cn.javass.spring.chapter3. SingletonTest”中的“testSingleton”測試方法,裡邊詳細演示了一個ClassLoader有一個單例例項。

1)  通過登錄檔方式: 首先將需要單例的例項通過唯一鍵註冊到登錄檔,然後通過鍵來獲取單例,讓我們直接看實現吧,注意本登錄檔實現了Spring介面“SingletonBeanRegistry”,該介面定義了操作共享的單例物件,Spring容器實現將實現此介面;所以共享單例物件通過“registerSingleton”方法註冊,通過“getSingleton”方法獲取,消除了程式設計方式單例,注意在實現中不考慮併發:

java程式碼:
  1. package cn.javass.spring.chapter3;  
  2. import java.util.HashMap;  
  3. import java.util.Map;  
  4. import org.springframework.beans.factory.config.SingletonBeanRegistry;  
  5. publicclass SingletonBeanRegister implements SingletonBeanRegistry {  
  6.     //單例Bean快取池,此處不考慮併發
  7.     privatefinal Map<String, Object> BEANS = new HashMap<String, Object>();  
  8.     publicboolean containsSingleton(String beanName) {  
  9.         return BEANS.containsKey(beanName);  
  10.     }  
  11.     public Object getSingleton(String beanName) {  
  12.         return BEANS.get(beanName);  
  13.     }  
  14.     @Override
  15.     publicint getSingletonCount() {  
  16.         return BEANS.size();  
  17.     }  
  18.     @Override
  19.     public String[] getSingletonNames() {  
  20.         return BEANS.keySet().toArray(new String[0]);  
  21.     }  
  22.     @Override
  23.     publicvoid registerSingleton(String beanName, Object bean) {  
  24.         if(BEANS.containsKey(beanName)) {  
  25.             thrownew RuntimeException("[" + beanName + "] 已存在");  
  26.         }  
  27.         BEANS.put(beanName, bean);  
  28. }  
  29. }  

Spring是登錄檔單例設計模式的實現,消除了程式設計式單例,而且對程式碼是非入侵式。

接下來讓我們看看在Spring中如何配置單例Bean吧,在Spring容器中如果沒指定作用域預設就是“singleton”,配置方式通過scope屬性配置,具體配置如下:

java程式碼:
  1. <bean  class="cn.javass.spring.chapter3.bean.Printer" scope="singleton"/>  

       Spring管理單例物件在Spring容器中儲存如圖3-5所示,Spring不僅會快取單例物件,Bean定義也是會快取的,對於惰性初始化的物件是在首次使用時根據Bean定義建立並存放於單例快取池。

 

圖3-5 單例處理

二、prototype:即原型,指每次向Spring容器請求獲取Bean都返回一個全新的Bean,相對於“singleton”來說就是不快取Bean,每次都是一個根據Bean定義建立的全新Bean。

GoF原型設計模式,指用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。

Spring中的原型和GoF中介紹的原型含義是不一樣的:

         GoF通過用原型例項指定建立物件的種類,而Spring容器用Bean定義指定建立物件的種類;

         GoF通過拷貝這些原型建立新的物件,而Spring容器根據Bean定義建立新物件。

其相同地方都是根據某些東西建立新東西,而且GoF原型必須顯示實現克隆操作,屬於侵入式,而Spring容器只需配置即可,屬於非侵入式。

接下來讓我們看看Spring如何實現原型呢?

1)首先讓我們來定義Bean“原型”:Bean定義,所有物件將根據Bean定義建立;在此我們只是簡單示例一下,不會涉及依賴注入等複雜實現:BeanDefinition類定義屬性“class”表示原型類,“id”表示唯一標識,“scope”表示作用域,具體如下:

java程式碼:
  1. package cn.javass.spring.chapter3;  
  2. publicclass BeanDefinition {  
  3.     //單例
  4.     publicstaticfinalint SCOPE_SINGLETON = 0;  
  5.     //原型
  6.     publicstaticfinalint SCOPE_PROTOTYPE = 1;  
  7.     //唯一標識
  8.     private String id;  
  9.     //class全限定名
  10.     private String clazz;  
  11.     //作用域
  12. privateint scope = SCOPE_SINGLETON;  
  13.     //鑑於篇幅,省略setter和getter方法;
  14. }  

2)接下來讓我們看看Bean定義登錄檔,類似於單例登錄檔:

java程式碼:
  1. package cn.javass.spring.chapter3;  
  2. import java.util.HashMap;  
  3. import java.util.Map;  
  4. publicclass BeanDifinitionRegister {  
  5.     //bean定義快取,此處不考慮併發問題
  6. privatefinal Map<String, BeanDefinition> DEFINITIONS =  
  7.  new HashMap<String, BeanDefinition>();  
  8.     publicvoid registerBeanDefinition(String beanName, BeanDefinition bd) {  
  9.         //1.本實現不允許覆蓋Bean定義
  10.         if(DEFINITIONS.containsKey(bd.getId())) {  
  11.             thrownew RuntimeException("已存在Bean定義,此實現不允許覆蓋");  
  12.         }  
  13.         //2.將Bean定義放入Bean定義快取池
  14.         DEFINITIONS.put(bd.getId(), bd);  
  15.     }  
  16.     public BeanDefinition getBeanDefinition(String beanName) {  
  17.         return DEFINITIONS.get(beanName);  
  18.     }  
  19. publicboolean containsBeanDefinition(String beanName) {        
  20.  return DEFINITIONS.containsKey(beanName);  
  21.     }  
  22. }  

3)接下來應該來定義BeanFactory了:

java程式碼:
  1. package cn.javass.spring.chapter3;  
  2. import org.springframework.beans.factory.config.SingletonBeanRegistry;  
  3. publicclass DefaultBeanFactory {  
  4.     //Bean定義登錄檔
  5.     private BeanDifinitionRegister DEFINITIONS = new BeanDifinitionRegister();  
  6.     //單例登錄檔
  7.     privatefinal SingletonBeanRegistry SINGLETONS = new SingletonBeanRegister();  
  8.     public Object getBean(String beanName) {  
  9.         //1.驗證Bean定義是否存在
  10.         if(!DEFINITIONS.containsBeanDefinition(beanName)) {  
  11.             thrownew RuntimeException("不存在[" + beanName + "]Bean定義");  
  12.         }  
  13.         //2.獲取Bean定義
  14.         BeanDefinition bd = DEFINITIONS.getBeanDefinition(beanName);  
  15.         //3.是否該Bean定義是單例作用域
  16.         if(bd.getScope() == BeanDefinition.SCOPE_SINGLETON) {  
  17.             //3.1 如果單例登錄檔包含Bean,則直接返回該Bean
  18.             if(SINGLETONS.containsSingleton(beanName)) {  
  19.                 return SINGLETONS.getSingleton(beanName);  
  20.             }  
  21.             //3.2單例登錄檔不包含該Bean,
  22.             //則建立並註冊到單例登錄檔,從而快取
  23.             SINGLETONS.registerSingleton(beanName, createBean(bd));  
  24.             return SINGLETONS.getSingleton(beanName);  
  25.         }  
  26.         //4.如果是原型Bean定義,則直接返回根據Bean定義建立的新Bean,
  27. //每次都是新的,無快取
  28.         if(bd.getScope() == BeanDefinition.SCOPE_PROTOTYPE) {  
  29.             return createBean(bd);  
  30.         }  
  31.         //5.其他情況錯誤的Bean定義
  32.         thrownew RuntimeException("錯誤的Bean定義");  
  33.     }  
java程式碼:
  1. publicvoid registerBeanDefinition(BeanDefinition bd) {  
  2.      DEFINITIONS.registerBeanDefinition(bd.getId(), bd);  
  3.  }  
  4.  private Object createBean(BeanDefinition bd) {  
  5.      //根據Bean定義建立Bean
  6.      try {  
  7.          Class clazz = Class.forName(bd.getClazz());  
  8.          //通過反射使用無引數構造器建立Bean
  9.          return clazz.getConstructor().newInstance();  
  10.      } catch (ClassNotFoundException e) {  
  11.          thrownew RuntimeException("沒有找到Bean[" + bd.getId() + "]類");  
  12.      } catch (Exception e) {  
  13.          thrownew RuntimeException("建立Bean[" + bd.getId() + "]失敗");  
  14.      }  
  15.  }  

       其中方法getBean用於獲取根據beanName對於的Bean定義建立的物件,有單例和原型兩類Bean;registerBeanDefinition方法用於註冊Bean定義,私有方法createBean用於根據Bean定義中的型別資訊建立Bean。

3)測試一下吧,在此我們只測試原型作用域Bean,對於每次從Bean工廠中獲取的Bean都是一個全新的物件,程式碼片段(BeanFatoryTest)如下:

java程式碼:
  1. @Test
  2. publicvoid testPrototype () throws Exception {  
  3. //1.建立Bean工廠
  4. DefaultBeanFactory bf = new DefaultBeanFactory();  
  5. //2.建立原型 Bean定義
  6. BeanDefinition bd = new BeanDefinition();  
  7. bd.setId("bean");  
  8. bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);  
  9. bd.setClazz(HelloImpl2.class.getName());  
  10. bf.registerBeanDefinition(bd);  
  11. //對於原型Bean每次應該返回一個全新的Bean
  12. System.out.println(bf.getBean("bean") != bf.getBean("bean"));  
  13. }  

最後讓我們看看如何在Spring中進行配置吧,只需指定<bean>標籤屬性“scope”屬性為“prototype”即可:

java程式碼:
  1. <bean class="cn.javass.spring.chapter3.bean.Printer" />  

       Spring管理原型物件在Spring容器中儲存如圖3-6所示,Spring不會快取原型物件,而是根據Bean定義每次請求返回一個全新的Bean:

 

圖3-6 原型處理

       單例和原型作用域我們已經講完,接下來讓我們學習一些在Web應用中有哪些作用域:

3.4.2  Web應用中的作用域

在Web應用中,我們可能需要將資料儲存到request、session、global session。因此Spring提供了三種Web作用域:request、session、globalSession。

一、request作用域:表示每個請求需要容器建立一個全新Bean。比如提交表單的資料必須是對每次請求新建一個Bean來保持這些表單資料,請求結束釋放這些資料。

二、session作用域:表示每個會話需要容器建立一個全新Bean。比如對於每個使用者一般會有一個會話,該使用者的使用者資訊需要儲存到會話中,此時可以將該Bean配置為web作用域。

三、globalSession:類似於session作用域,只是其用於portlet環境的web應用。如果在非portlet環境將視為session作用域。

配置方式和基本的作用域相同,只是必須要有web環境支援,並配置相應的容器監聽器或攔截器從而能應用這些作用域,我們會在整合web時講解具體使用,大家只需要知道有這些作用域就可以了。

3.4.4 自定義作用域

在日常程式開發中,幾乎用不到自定義作用域,除非又必要才進行自定義作用域。

首先讓我們看下Scope介面吧:

java程式碼:
  1. package org.springframework.beans.factory.config;  
  2. import org.springframework.beans.factory.ObjectFactory;  
  3. publicinterface Scope {  
  4.        Object get(String name, ObjectFactory<?> objectFactory);  
  5.        Object remove(String name);  
  6.        void registerDestructionCallback(String name, Runnable callback);  
  7.        Object resolveContextualObject(String key);  
  8.        String getConversationId();  
  9. }  

1)Object get(String name, ObjectFactory<?> objectFactory):用於從作用域中獲取Bean,其中引數objectFactory是當在當前作用域沒找到合適Bean時使用它建立一個新的Bean;

2)void registerDestructionCallback(String name, Runnable callback):用於註冊銷燬回撥,如果想要銷燬相應的物件則由Spring容器註冊相應的銷燬回撥,而由自定義作用域選擇是不是要銷燬相應的物件;

3)Object resolveContextualObject(String key):用於解析相應的上下文資料,比如request作用域將返回request中的屬性。

4)String getConversationId():作用域的會話標識,比如session作用域將是sessionId。

java程式碼:
  1. package cn.javass.spring.chapter3;  
  2. import java.util.HashMap;  
  3. import java.util.Map;  
  4. import org.springframework.beans.factory.ObjectFactory;  
  5. import org.springframework.beans.factory.config.Scope;  
  6. publicclass ThreadScope implements Scope {  
  7. privatefinal ThreadLocal<Map<String, Object>> THREAD_SCOPE =  
  8. new ThreadLocal<Map<String, Object>>() {  
  9.       protected Map<String, Object> initialValue() {  
  10.           //用於存放執行緒相關Bean
  11.           returnnew HashMap<String, Object>();  
  12.       }  
  13.     };  

       讓我們來實現個簡單的thread作用域,該作用域內建立的物件將繫結到ThreadLocal內。

java程式碼:
  1. @Override
  2.     public Object get(String name, ObjectFactory<?> objectFactory) {  
  3.         //如果當前執行緒已經綁定了相應Bean,直接返回
  4.         if(THREAD_SCOPE.get().containsKey(name)) {  
  5.            return THREAD_SCOPE.get().get(name);  
  6.         }  
  7.         //使用objectFactory建立Bean並繫結到當前執行緒上
  8.         THREAD_SCOPE.get().put(name, objectFactory.getObject());  
  9.         return THREAD_SCOPE.get().get(name);  
  10.     }  
  11.     @Override
  12.     public String getConversationId() {  
  13.         returnnull;  
  14.     }  
  15.     @Override
  16.     publicvoid registerDestructionCallback(String name, Runnable callback) {  
  17.         //此處不實現就代表類似proytotype,容器返回給使用者後就不管了
  18.     }  
  19.     @Override
  20.     public Object remove(String name) {  
  21.         return THREAD_SCOPE.get().remove(name);  
  22.     }  
  23.     @Override
  24.     public Object resolveContextualObject(String key) {  
  25.         returnnull;  
  26.     }  
  27. }  

Scope已經實現了,讓我們將其註冊到Spring容器,使其發揮作用:

java程式碼:
  1. <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">  
  2.         <property name="scopes">  
  3.            <map><entry>  
  4.                    <!-- 指定scope關鍵字 --><key><value>thread</value></key>  
  5.                    <!-- scope實現 -->      <bean class="cn.javass.spring.chapter3.ThreadScope"/>  
  6.            </entry></map>      
  7.         </property>  
  8.     </bean>  

通過CustomScopeConfigurer的scopes屬性註冊自定義作用域實現,在此需要指定使用作用域的關鍵字“thread”,並指定自定義作用域實現。來讓我們來定義一個“thread”作用域的Bean,配置(chapter3/threadScope.xml)如下:

java程式碼:
  1. <bean id="helloApi"
  2.     class="cn.javass.spring.chapter2.helloworld.HelloImpl"
  3.     scope="thread"/>  

最後測試(cn.javass.spring.chapter3.ThreadScopeTest)一下吧,首先在一個執行緒中測試,在同一執行緒中獲取的Bean應該是一樣的;再讓我們開啟兩個執行緒,然後應該這兩個執行緒建立的Bean是不一樣:

自定義作用域實現其實是非常簡單的,其實複雜的是如果需要銷燬Bean,自定義作用域如何正確的銷燬Bean。