【第三章】 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程式碼:- package cn.javass.spring.chapter3.bean;
- publicclass Singleton {
- //1.私有化構造器
- private Singleton() {}
- //2.單例快取者,惰性初始化,第一次使用時初始化
- privatestaticclass InstanceHolder {
- privatestaticfinal Singleton INSTANCE = new Singleton();
- }
- //3.提供全域性訪問點
- publicstatic Singleton getInstance() {
- return InstanceHolder.INSTANCE;
- }
- //4.提供一個計數器來驗證一個ClassLoader一個例項
- privateint 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;
- publicclass SingletonBeanRegister implements SingletonBeanRegistry {
- //單例Bean快取池,此處不考慮併發
- privatefinal Map<String, Object> BEANS = new HashMap<String, Object>();
- publicboolean containsSingleton(String beanName) {
- return BEANS.containsKey(beanName);
- }
- public Object getSingleton(String beanName) {
- return BEANS.get(beanName);
- }
- @Override
- publicint getSingletonCount() {
- return BEANS.size();
- }
- @Override
- public String[] getSingletonNames() {
- return BEANS.keySet().toArray(new String[0]);
- }
- @Override
- publicvoid registerSingleton(String beanName, Object bean) {
- if(BEANS.containsKey(beanName)) {
- thrownew 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;
- publicclass BeanDefinition {
- //單例
- publicstaticfinalint SCOPE_SINGLETON = 0;
- //原型
- publicstaticfinalint SCOPE_PROTOTYPE = 1;
- //唯一標識
- private String id;
- //class全限定名
- private String clazz;
- //作用域
- privateint scope = SCOPE_SINGLETON;
- //鑑於篇幅,省略setter和getter方法;
- }
2)接下來讓我們看看Bean定義登錄檔,類似於單例登錄檔:
java程式碼:- package cn.javass.spring.chapter3;
- import java.util.HashMap;
- import java.util.Map;
- publicclass BeanDifinitionRegister {
- //bean定義快取,此處不考慮併發問題
- privatefinal Map<String, BeanDefinition> DEFINITIONS =
- new HashMap<String, BeanDefinition>();
- publicvoid registerBeanDefinition(String beanName, BeanDefinition bd) {
- //1.本實現不允許覆蓋Bean定義
- if(DEFINITIONS.containsKey(bd.getId())) {
- thrownew RuntimeException("已存在Bean定義,此實現不允許覆蓋");
- }
- //2.將Bean定義放入Bean定義快取池
- DEFINITIONS.put(bd.getId(), bd);
- }
- public BeanDefinition getBeanDefinition(String beanName) {
- return DEFINITIONS.get(beanName);
- }
- publicboolean containsBeanDefinition(String beanName) {
- return DEFINITIONS.containsKey(beanName);
- }
- }
3)接下來應該來定義BeanFactory了:
java程式碼:- package cn.javass.spring.chapter3;
- import org.springframework.beans.factory.config.SingletonBeanRegistry;
- publicclass DefaultBeanFactory {
- //Bean定義登錄檔
- private BeanDifinitionRegister DEFINITIONS = new BeanDifinitionRegister();
- //單例登錄檔
- privatefinal SingletonBeanRegistry SINGLETONS = new SingletonBeanRegister();
- public Object getBean(String beanName) {
- //1.驗證Bean定義是否存在
- if(!DEFINITIONS.containsBeanDefinition(beanName)) {
- thrownew 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定義
- thrownew RuntimeException("錯誤的Bean定義");
- }
- publicvoid 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) {
- thrownew RuntimeException("沒有找到Bean[" + bd.getId() + "]類");
- } catch (Exception e) {
- thrownew RuntimeException("建立Bean[" + bd.getId() + "]失敗");
- }
- }
其中方法getBean用於獲取根據beanName對於的Bean定義建立的物件,有單例和原型兩類Bean;registerBeanDefinition方法用於註冊Bean定義,私有方法createBean用於根據Bean定義中的型別資訊建立Bean。
3)測試一下吧,在此我們只測試原型作用域Bean,對於每次從Bean工廠中獲取的Bean都是一個全新的物件,程式碼片段(BeanFatoryTest)如下:
java程式碼:- @Test
- publicvoid 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;
- publicinterface 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;
- publicclass ThreadScope implements Scope {
- privatefinal ThreadLocal<Map<String, Object>> THREAD_SCOPE =
- new ThreadLocal<Map<String, Object>>() {
- protected Map<String, Object> initialValue() {
- //用於存放執行緒相關Bean
- returnnew 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() {
- returnnull;
- }
- @Override
- publicvoid registerDestructionCallback(String name, Runnable callback) {
- //此處不實現就代表類似proytotype,容器返回給使用者後就不管了
- }
- @Override
- public Object remove(String name) {
- return THREAD_SCOPE.get().remove(name);
- }
- @Override
- public Object resolveContextualObject(String key) {
- returnnull;
- }
- }
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。