什么是作用域呢?即“scope”,在面向對象程序設計中一般指對象或變量之間的可見范圍。而在SPRing容器中是指其創建的Bean對象相對于其他Bean對象的請求可見范圍。
Spring提供“singleton”和“prototype”兩種基本作用域,另外提供“request”、“session”、“global session”三種web作用域;Spring還允許用戶定制自己的作用域。
一、singleton:指“singleton”作用域的Bean只會在每個Spring IoC容器中存在一個實例,而且其完整生命周期完全由Spring容器管理。對于所有獲取該Bean的操作Spring容器將只返回同一個Bean。
GoF單例設計模式指“保證一個類僅有一個實例,并提供一個訪問它的全局訪問點”,介紹了兩種實現:通過在類上定義靜態屬性保持該實例和通過注冊表方式。
1)通過在類上定義靜態屬性保持該實例:一般指一個java虛擬機 ClassLoader裝載的類只有一個實例,一般通過類靜態屬性保持該實例,這樣就造成需要單例的類都需要按照單例設計模式進行編碼;Spring沒采用這種方式,因為該方式屬于侵入式設計;代碼樣例如下:
java代碼:查看復制到剪貼板打印package cn.javass.spring.chapter3.bean; public class Singleton { //1.私有化構造器 private Singleton() {} //2.單例緩存者,惰性初始化,第一次使用時初始化 private static class InstanceHolder { private static final Singleton INSTANCE = new Singleton(); } //3.提供全局訪問點 public static Singleton getInstance() { return InstanceHolder.INSTANCE; } //4.提供一個計數器來驗證一個ClassLoader一個實例 private int counter=0; }
以上定義個了個單例類,首先要私有化類構造器;其次使用InstanceHolder靜態內部類持有單例對象,這樣可以得到惰性初始化好處;最后提供全局訪問點getInstance,使得需要該單例實例的對象能獲取到;我們在此還提供了一個counter計數器來驗證一個ClassLoader一個實例。具體一個ClassLoader有一個單例實例測試請參考代碼“cn.javass.spring.chapter3. SingletonTest”中的“testSingleton”測試方法,里邊詳細演示了一個ClassLoader有一個單例實例。
1) 通過注冊表方式: 首先將需要單例的實例通過唯一鍵注冊到注冊表,然后通過鍵來獲取單例,讓我們直接看實現吧,注意本注冊表實現了Spring接口“SingletonBeanRegistry”,該接口定義了操作共享的單例對象,Spring容器實現將實現此接口;所以共享單例對象通過“registerSingleton”方法注冊,通過“getSingleton”方法獲取,消除了編程方式單例,注意在實現中不考慮并發:
java代碼:查看復制到剪貼板打印package cn.javass.spring.chapter3; import java.util.HashMap; import java.util.Map; import org.springframework.beans.factory.config.SingletonBeanRegistry; public class SingletonBeanRegister implements SingletonBeanRegistry { //單例Bean緩存池,此處不考慮并發 private final Map<String, Object> BEANS = new HashMap<String, Object>(); public boolean containsSingleton(String beanName) { return BEANS.containsKey(beanName); } public Object getSingleton(String beanName) { return BEANS.get(beanName); } @Override public int getSingletonCount() { return BEANS.size(); } @Override public String[] getSingletonNames() { return BEANS.keySet().toArray(new String[0]); } @Override public void registerSingleton(String beanName, Object bean) { if(BEANS.containsKey(beanName)) { throw new RuntimeException("[" + beanName + "] 已存在"); } BEANS.put(beanName, bean); } }
Spring是注冊表單例設計模式的實現,消除了編程式單例,而且對代碼是非入侵式。
接下來讓我們看看在Spring中如何配置單例Bean吧,在Spring容器中如果沒指定作用域默認就是“singleton”,配置方式通過scope屬性配置,具體配置如下:
java代碼:查看復制到剪貼板打印<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代碼:查看復制到剪貼板打印package cn.javass.spring.chapter3; public class BeanDefinition { //單例 public static final int SCOPE_SINGLETON = 0; //原型 public static final int SCOPE_PROTOTYPE = 1; //唯一標識 private String id; //class全限定名 private String clazz; //作用域 private int scope = SCOPE_SINGLETON; //鑒于篇幅,省略setter和getter方法; }
2)接下來讓我們看看Bean定義注冊表,類似于單例注冊表:
java代碼:查看復制到剪貼板打印package cn.javass.spring.chapter3; import java.util.HashMap; import java.util.Map; public class BeanDifinitionRegister { //bean定義緩存,此處不考慮并發問題 private final Map<String, BeanDefinition> DEFINITIONS = new HashMap<String, BeanDefinition>(); public void registerBeanDefinition(String beanName, BeanDefinition bd) { //1.本實現不允許覆蓋Bean定義 if(DEFINITIONS.containsKey(bd.getId())) { throw new RuntimeException("已存在Bean定義,此實現不允許覆蓋"); } //2.將Bean定義放入Bean定義緩存池 DEFINITIONS.put(bd.getId(), bd); } public BeanDefinition getBeanDefinition(String beanName) { return DEFINITIONS.get(beanName); } public boolean containsBeanDefinition(String beanName) { return DEFINITIONS.containsKey(beanName); } }
3)接下來應該來定義BeanFactory了:
java代碼:查看復制到剪貼板打印package cn.javass.spring.chapter3; import org.springframework.beans.factory.config.SingletonBeanRegistry; public class DefaultBeanFactory { //Bean定義注冊表 private BeanDifinitionRegister DEFINITIONS = new BeanDifinitionRegister(); //單例注冊表 private final SingletonBeanRegistry SINGLETONS = new SingletonBeanRegister(); public Object getBean(String beanName) { //1.驗證Bean定義是否存在 if(!DEFINITIONS.containsBeanDefinition(beanName)) { throw new RuntimeException("不存在[" + beanName + "]Bean定義"); } //2.獲取Bean定義 BeanDefinition bd = DEFINITIONS.getBeanDefinition(beanName); //3.是否該Bean定義是單例作用域 if(bd.getScope() == BeanDefinition.SCOPE_SINGLETON) { //3.1 如果單例注冊表包含Bean,則直接返回該Bean if(SINGLETONS.containsSingleton(beanName)) { return SINGLETONS.getSingleton(beanName); } //3.2單例注冊表不包含該Bean, //則創建并注冊到單例注冊表,從而緩存 SINGLETONS.registerSingleton(beanName, createBean(bd)); return SINGLETONS.getSingleton(beanName); } //4.如果是原型Bean定義,則直接返回根據Bean定義創建的新Bean, //每次都是新的,無緩存 if(bd.getScope() == BeanDefinition.SCOPE_PROTOTYPE) { return createBean(bd); } //5.其他情況錯誤的Bean定義 throw new RuntimeException("錯誤的Bean定義"); }
java代碼:查看復制到剪貼板打印public void registerBeanDefinition(BeanDefinition bd) { DEFINITIONS.registerBeanDefinition(bd.getId(), bd); } private Object createBean(BeanDefinition bd) { //根據Bean定義創建Bean try { Class clazz = Class.forName(bd.getClazz()); //通過反射使用無參數構造器創建Bean return clazz.getConstructor().newInstance(); } catch (ClassNotFoundException e) { throw new RuntimeException("沒有找到Bean[" + bd.getId() + "]類"); } catch (Exception e) { throw new RuntimeException("創建Bean[" + bd.getId() + "]失敗"); } }
其中方法getBean用于獲取根據beanName對于的Bean定義創建的對象,有單例和原型兩類Bean;registerBeanDefinition方法用于注冊Bean定義,私有方法createBean用于根據Bean定義中的類型信息創建Bean。
3)測試一下吧,在此我們只測試原型作用域Bean,對于每次從Bean工廠中獲取的Bean都是一個全新的對象,代碼片段(BeanFatoryTest)如下:
java代碼:查看復制到剪貼板打印@Test public void testPrototype () throws Exception { //1.創建Bean工廠 DefaultBeanFactory bf = new DefaultBeanFactory(); //2.創建原型 Bean定義 BeanDefinition bd = new BeanDefinition(); bd.setId("bean"); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bd.setClazz(HelloImpl2.class.getName()); bf.registerBeanDefinition(bd); //對于原型Bean每次應該返回一個全新的Bean System.out.println(bf.getBean("bean") != bf.getBean("bean")); }
最后讓我們看看如何在Spring中進行配置吧,只需指定<bean>標簽屬性“scope”屬性為“prototype”即可:
java代碼:查看復制到剪貼板打印<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代碼:查看復制到剪貼板打印package org.springframework.beans.factory.config; import org.springframework.beans.factory.ObjectFactory; public interface Scope { Object get(String name, ObjectFactory<?> objectFactory); Object remove(String name); void registerDestructionCallback(String name, Runnable callback); Object resolveContextualObject(String key); String getConversationId(); }
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代碼:查看復制到剪貼板打印package cn.javass.spring.chapter3; import java.util.HashMap; import java.util.Map; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.config.Scope; public class ThreadScope implements Scope { private final ThreadLocal<Map<String, Object>> THREAD_SCOPE = new ThreadLocal<Map<String, Object>>() { protected Map<String, Object> initialValue() { //用于存放線程相關Bean return new HashMap<String, Object>(); } };
讓我們來實現個簡單的thread作用域,該作用域內創建的對象將綁定到ThreadLocal內。
java代碼:查看復制到剪貼板打印 @Override public Object get(String name, ObjectFactory<?> objectFactory) { //如果當前線程已經綁定了相應Bean,直接返回 if(THREAD_SCOPE.get().containsKey(name)) { return THREAD_SCOPE.get().get(name); } //使用objectFactory創建Bean并綁定到當前線程上 THREAD_SCOPE.get().put(name, objectFactory.getObject()); return THREAD_SCOPE.get().get(name); } @Override public String getConversationId() { return null; } @Override public void registerDestructionCallback(String name, Runnable callback) { //此處不實現就代表類似proytotype,容器返回給用戶后就不管了 } @Override public Object remove(String name) { return THREAD_SCOPE.get().remove(name); } @Override public Object resolveContextualObject(String key) { return null; } }
Scope已經實現了,讓我們將其注冊到Spring容器,使其發揮作用:
java代碼:查看復制到剪貼板打印<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map><entry> <!-- 指定scope關鍵字 --><key><value>thread</value></key> <!-- scope實現 --> <bean class="cn.javass.spring.chapter3.ThreadScope"/> </entry></map> </property> </bean>
通過CustomScopeConfigurer的scopes屬性注冊自定義作用域實現,在此需要指定使用作用域的關鍵字“thread”,并指定自定義作用域實現。來讓我們來定義一個“thread”作用域的Bean,配置(chapter3/threadScope.xml)如下:
java代碼:查看復制到剪貼板打印<bean id="helloApi" class="cn.javass.spring.chapter2.helloworld.HelloImpl" scope="thread"/>
最后測試(cn.javass.spring.chapter3.ThreadScopeTest)一下吧,首先在一個線程中測試,在同一線程中獲取的Bean應該是一樣的;再讓我們開啟兩個線程,然后應該這兩個線程創建的Bean是不一樣:
自定義作用域實現其實是非常簡單的,其實復雜的是如果需要銷毀Bean,自定義作用域如何正確的銷毀Bean。
原創內容 】
新聞熱點
疑難解答