1. 程式人生 > >spring 如何實現注入多個數據源,並且可以同時使用這多個數據源!

spring 如何實現注入多個數據源,並且可以同時使用這多個數據源!

source address: http://bbs.ibeifeng.com/simple/index.php?t16636.html

如何在spring框架中解決多資料來源的問題[轉貼]
在我們的專案中遇到這樣一個問題:我們的專案需要連線多個數據庫,而且不同的客戶在每次訪問中根據需要會去訪問不同的資料庫。我們以往在spring和hibernate框架中總是配置一個數據源,因而sessionFactory的 dataSource屬性總是指向這個資料來源並且恆定不變,所有DAO在使用sessionFactory的時候都是通過這個資料來源訪問資料庫。但是現在,由於專案的需要,我們的DAO在訪問sessionFactory的時候都不得不在多個數據源中不斷切換,問題就出現了:如何讓 sessionFactory在執行資料持久化的時候,根據客戶的需求能夠動態切換不同的資料來源?我們能不能在spring的框架下通過少量修改得到解決?是否有什麼設計模式可以利用呢? 

問題的分析
我首先想到在spring的applicationContext中配置所有的dataSource。這些dataSource可能是各種不同型別的,比如不同的資料庫:Oracle、SQL Server、MySQL等,也可能是不同的資料來源:比如apache 提供的org.apache.commons.dbcp.BasicDataSource、spring提供的 org.springframework.jndi.JndiObjectFactoryBean等。然後sessionFactory根據客戶的每次請求,將dataSource屬性設定成不同的資料來源,以到達切換資料來源的目的。

但是,我很快發現一個問題:當多使用者同時併發訪問資料庫的時候會出現資源爭用的問題。這都是“單例模式”惹的禍。眾所周知,我們在使用spring框架的時候,在beanFactory中註冊的bean基本上都是採用單例模式,即spring在啟動的時候,這些bean就裝載到記憶體中,並且每個bean在整個專案中只存在一個物件。正因為只存在一個物件,物件的所有屬性,更準確說是例項變數,表現得就如同是個靜態變數(實際上“靜態”與“單例”往往是非常相似的兩個東西,我們常常用“靜態”來實現“單例”)。拿我們的問題來說,sessionFactory在整個專案中只有一個物件,它的例項變數dataSource也就只有一個,就如同一個靜態變數一般。如果不同的使用者都不斷地去修改dataSource的值,必然會出現多使用者爭用一個變數的問題,對系統產生隱患。

通過以上的分析,解決多資料來源訪問問題的關鍵,就集中在sessionFactory在執行資料持久化的時候,能夠通過某段程式碼去根據客戶的需要動態切換資料來源,並解決資源爭用的問題。

問題的解決
(一)            採用Decorator設計模式
要解決這個問題,我的思路鎖定在了這個dataSource上了。如果sessionFactory指向的dataSource可以根據客戶的需求去連線客戶所需要的真正的資料來源,即提供動態切換資料來源的功能,那麼問題就解決了。那麼我們怎麼做呢?去修改那些我們要使用的dataSource原始碼嗎?這顯然不是一個好的方案,我們希望我們的修改與原dataSource程式碼是分離的。根據以上的分析,使用GoF設計模式中的Decorator模式(裝飾者模式)應當是我們可以選擇的最佳方案。

什麼是“Decorator模式”?簡單點兒說就是當我們需要修改原有的功能,但我們又不願直接去修改原有的程式碼時,設計一個Decorator套在原有程式碼外面。當我們使用Decorator的時候與原類完全一樣,當Decorator的某些功能卻已經修改為了我們需要修改的功能。Decorator模式的結構如圖。



我們本來需要修改圖中所有具體的Component 類的一些功能,但卻並不是去直接修改它們的程式碼,而是在它們的外面增加一個Decorator。Decorator與具體的Component類都是繼承的AbstractComponent,因此它長得和具體的Component類一樣,也就是說我們在使用Decorator的時候就如同在使用 ConcreteComponentA或者ConcreteComponentB一樣,甚至那些使用ConcreteComponentA或者 ConcreteComponentB的客戶程式都不知道它們用的類已經改為了Decorator,但是Decorator已經對具體的 Component類的部分方法進行了修改,執行這些方法的結果已經不同了。

(二)            設計MultiDataSource類
現在回到我們的問題,我們需要對dataSource的功能進行變更,但又不希望修改dataSource中的任何程式碼。我這裡指的dataSource是所有實現javax.sql.DataSource介面的類,我們常用的包括apache 提供的org.apache.commons.dbcp.BasicDataSource、spring提供的 org.springframework.jndi.JndiObjectFactoryBean等,這些類我們不可能修改它們本身,更不可能對它們一個個地修改以實現動態分配資料來源的功能,同時,我們又希望使用dataSource的sessionFactory根本就感覺不到這樣的變化。 Decorator模式就正是解決這個問題的設計模式。

首先寫一個Decorator類,我取名叫MultiDataSource,通過它來動態切換資料來源。同時在配置檔案中將sessionFactory的dataSource屬性由原來的某個具體的dataSource改為MultiDataSource。如圖:



對比原Decorator模式,AbstractComponent是一個抽象類,但在這裡我們可以將這個抽象類用介面來代替,即DataSource介面,而ConcreteComponent就是那些DataSource的實現類,如BasicDataSource、 JndiObjectFactoryBean等。MultiDataSource封裝了具體的dataSource,並實現了資料來源動態切換:


java 程式碼
public class MultiDataSource implements DataSource {   
    private DataSource dataSource = null;   
public MultiDataSource(DataSource dataSource){   
        this.dataSource = dataSource;   
    }   
    /* (non-Javadoc) 
    * @see javax.sql.DataSource#getConnection() 
    */ 
    public Connection getConnection() throws SQLException {   
        return getDataSource().getConnection();   
    }   
    //其它DataSource介面應當實現的方法   
 
    public DataSource getDataSource(){   
        return this.dataSource;   
        }   
    }   
    public void setDataSource(DataSource dataSource) {   
        this.dataSource = dataSource;   
    }   
}   
客戶在發出請求的時候,將dataSourceName放到request中,然後把request中的資料來源名通過呼叫new MultiDataSource(dataSource)時可以告訴MultiDataSource客戶需要的資料來源,就可以實現動態切換資料來源了。但細心的朋友會發現這在單例的情況下就是問題的,因為MultiDataSource在系統中只有一個物件,它的例項變數dataSource也只有一個,就如同一個靜態變數一般。正因為如此,單例模式讓許多設計模式都不得不需要更改,這將在我的《“單例”更改了我們的設計模式》中詳細討論。那麼,我們在單例模式下如何設計呢?

(三)            單例模式下的MultiDataSource
在單例模式下,由於我們在每次呼叫MultiDataSource的方法的時候,dataSource都可能是不同的,所以我們不能將dataSource放在例項變數 dataSource中,最簡單的方式就是在方法getDataSource()中增加引數,告訴MultiDataSource我到底呼叫的是哪個 dataSource:

java 程式碼
public DataSource getDataSource(String dataSourceName){   
        log.debug("dataSourceName:"+dataSourceName);   
        try{   
            if(dataSourceName==null||dataSourceName.equals("")){   
                return this.dataSource;   
            }   
            return (DataSource)this.applicationContext.getBean(dataSourceName);   
        }catch(NoSuchBeanDefinitionException ex){   
            throw new DaoException("There is not the dataSource 
        }   
    }   
值得一提的是,我需要的資料來源已經都在spring的配置檔案中註冊,dataSourceName就是其對應的id。

xml 程式碼
<bean id="dataSource1" 
    class="org.apache.commons.dbcp.BasicDataSource"> 
    <property name="driverClassName"> 
        <value>oracle.jdbc.driver.OracleDrivervalue> 
    property> 
    ......   
bean> 
<bean id="dataSource2" 
    class="org.apache.commons.dbcp.BasicDataSource"> 
    <property name="driverClassName"> 
        <value>oracle.jdbc.driver.OracleDrivervalue> 
    property>   
    ......   
bean>   
為了得到spring的ApplicationContext,MultiDataSource類必須實現介面org.springframework.context.ApplicationContextAware,並且實現方法:

java 程式碼
private ApplicationContext applicationContext = null;   
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {   
        this.applicationContext = applicationContext;   
    }   
如此這樣,我就可以通過this.applicationContext.getBean(dataSourceName)得到dataSource了。

(四)            通過執行緒傳遞dataSourceName
檢視以上設計,MultiDataSource依然無法執行,因為使用者在發出請求時,他需要連線什麼資料庫,其資料來源名是放在request中的,要將 request中的資料來源名傳給MultiDataSource,需要經過BUS和DAO,也就是說為了把資料來源名傳給 MultiDataSource,BUS和DAO的所有方法都要增加dataSourceName的引數,這是我們不願看到的。寫一個類,通過執行緒的方式跳過BUS和DAO直接傳遞給MultiDataSource是一個不錯的設計:



java 程式碼
public class SpObserver {   
    private static ThreadLocal local = new ThreadLocal();   
    public static void putSp(String sp) {   
        local.set(sp);   
    }   
    public static String getSp() {   
        return (String)local.get();   
    }   
}   
做一個filter,每次客戶發出請求的時候就呼叫SpObserver.petSp(dataSourceName),將request中的 dataSourceName傳遞給SpObserver物件。最後修改MultiDataSource的方法getDataSource():

java 程式碼
public DataSource getDataSource(){   
        String sp = SpObserver.getSp();   
        return getDataSource(sp);   
    }   
完整的MultiDataSource程式碼在附件中。

(五)            動態新增資料來源
通過以上方案,我們解決了動態分配資料來源的問題,但你可能提出疑問:方案中的資料來源都是配置在spring的ApplicationContext中,如果我在程式執行過程中動態新增資料來源怎麼辦?這確實是一個問題,而且在我們的專案中也確實遇到。spring的ApplicationContext是在專案啟動的時候載入的。載入以後,我們如何動態地載入新的bean到ApplicationContext中呢?我想到如果用spring自己的方法解決這個問題就好了。所幸的是,在檢視spring的原始碼後,我找到了這樣的程式碼,編寫了DynamicLoadBean類,只要呼叫loadBean()方法,就可以將某個或某幾個配置檔案中的bean載入到ApplicationContext中(見附件)。不通過配置檔案直接載入物件,在spring的原始碼中也有,感興趣的朋友可以自己研究。

(六)            在spring中配置
在完成了所有這些設計以後,我最後再嘮叨一句。我們應當在spring中做如下配置:

xml 程式碼
<bean id="dynamicLoadBean" class="com.htxx.service.dao.DynamicLoadBean">bean> 
<bean id="dataSource" class="com.htxx.service.dao.MultiDataSource"> 
        <property name="dataSource"> 
            <ref bean="dataSource1" /> 
        property> 
    bean> 
    <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> 
        <property name="dataSource"> 
            <ref bean="dataSource" /> 
        property> 
        ......   
    bean> 
其中dataSource屬性實際上更準確地說應當是defaultDataSource,即spring啟動時以及在客戶沒有指定資料來源時應當指定的預設資料來源。

該方案的優勢
以上方案與其它方案相比,它有哪些優勢呢?

首先,這個方案完全是在spring的框架下解決的,資料來源依然配置在spring的配置檔案中,sessionFactory依然去配置它的 dataSource屬性,它甚至都不知道dataSource的改變。唯一不同的是在真正的dataSource與sessionFactory之間增加了一個MultiDataSource。

其次,實現簡單,易於維護。這個方案雖然我說了這麼多東西,其實都是分析,真正需要我們寫的程式碼就只有MultiDataSource、SpObserver兩個類。MultiDataSource類真正要寫的只有getDataSource() 和getDataSource(sp)兩個方法,而SpObserver類更簡單了。實現越簡單,出錯的可能就越小,維護性就越高。

最後,這個方案可以使單資料來源與多資料來源相容。這個方案完全不影響BUS和DAO的編寫。如果我們的專案在開始之初是單資料來源的情況下開發,隨著專案的進行,需要變更為多資料來源,則只需要修改spring配置,並少量修改MVC層以便在請求中寫入需要的資料來源名,變更就完成了。如果我們的專案希望改回單資料來源,則只需要簡單修改配置檔案。這樣,為我們的專案將增加更多的彈性。

特別說明:例項中的DynamicLoadBean在web環境下執行會出錯,需要將類中AbstractApplicationContext改為 org.springframework.context.ConfigurableApplicationContext。

相關部落格:再析在spring框架中解決多資料來源的問題

example.rar (32.1 KB)
描述: 原始碼及示例
下載次數: 1214
再析在spring框架中解決多資料來源的問題
關鍵字: hibernate decorator 設計模式
在前面我寫了《如何在spring框架中解決多資料來源的問題》,通過設計模式中的Decorator模式在spring框架中解決多資料來源的問題,得到了許多網友的關注。在與網友探討該問題的過程中,我發現我的方案並不完善,它只解決了一部分問題。
總結多資料來源的問題,其實它需要分為以下三種情況:各個資料來源的資料結構不同、各個資料來源的資料結構相同、各個資料來源的資料結構部分相同又有部分不同。對於第二種情況,各個資料來源的資料結構相同,我們使用一個sessionFactory,而在sessionFactory中通過 MultiDataSource來動態切換資料來源,應當是一個不錯的方案,既解決了多個sessionFactory對相同的值物件重複裝載對記憶體的浪費,又使資料來源的切換對客戶程式透明,簡化了程式碼的實現和對客戶程式的影響。但是,對於第一種情況,各個資料來源的資料結構不同,運用這樣的方案存在潛在風險。

對於各個資料來源的資料結構不同的情況,使用一個sessionFactory而在這個sessionFactory中動態切換資料來源,可能造成資料訪問的張冠李戴。譬如,資料來源A有表T而資料來源B沒有,可能造成客戶程式在訪問表T的時候卻嘗試去連線資料來源B,因為客戶程式訪問哪個資料來源是在程式執行期間由客戶程式決定的,因此這樣的錯誤是很難發現的。也許客戶程式的一個不經意的錯誤就可能造成錯誤。解決這個問題的方法有兩個:一是嚴格要求客戶程式不要寫錯,這當然是可以做到的,但作為框架設計者,另一個解決方法是在框架中就避免出現這樣的情況。因此我祭出了 MultiSessionFactory的方案來解決各個資料來源的資料結構不同的多資料來源問題。

問題的分析
與 MultiDataSource的方案一樣,MultiSessionFactory同樣是在spring框架下呼叫 ApplicationContext的getBean()方法而不會另外建立beanFacoty,也同樣使用Decorator模式來處理切換的問題。MultiSessionFactory的物件關係如圖:



在該方案中,SessionFactory就是 Hibernate的org.hibernate.SessionFactory介面,Decorator就是 MultiSessionFactory,SessionFactory1和SessionFactory2往往是spring的 org.springframework.orm.hibernate3.LocalSessionFactoryBean。細心的朋友可能會注意,實際上LocalSessionFactoryBean並不是SessionFactory的實現,這個方案是否有問題呢?這個問題其實也一直困擾了我好久,最後我發現,我們通過ApplicationContext的getBean()得到一個LocalSessionFactoryBean的時候其實並不是真正地得到了它,而是得到了一個SessionFactory,因為spring為LocalSessionFactoryBean重寫了 getObject(),使其返回的是SessionFactory。一個簡單的明證就是,HibernateDaoSupport的 sessionFactory屬性的型別是SessionFactory,而我們在spring配置的時候注入的卻是 LocalSessionFactoryBean。
方案的實現
在整個這個方案中,我們需要實現的只有MultiSessionFactory類和我們可愛的Spserver,總共就兩個類,然後呢就是一些spring的配置,就完成了。

MultiSessionFactory實現了SessionFactory,同時為了得到AplicationContext而實現了ApplicationContextAware。MultiSessionFactory的程式碼如下:

java 程式碼
public class MultiSessionFactory implements SessionFactory, ApplicationContextAware {   
    private static final long serialVersionUID = 2064557324203496378L;   
    private static final Log log = LogFactory.getLog(MultiSessionFactory.class);   
    private ApplicationContext applicationContext = null;   
    private SessionFactory sessionFactory = null;   
    public ApplicationContext getApplicationContext() {   
        return applicationContext;   
    }   
    public void setApplicationContext(ApplicationContext applicationContext) {   
      this.applicationContext = applicationContext;   
    }   
    public SessionFactory getSessionFactory(String sessionFactoryName) {   
      log.debug("sessionFactoryName:"+sessionFactoryName);   
      try{   
          if(sessionFactoryName==null||sessionFactoryName.equals("")){   
              return sessionFactory;   
          }   
          return (SessionFactory)this.getApplicationContext().getBean(sessionFactoryName);   
      }catch(NoSuchBeanDefinitionException ex){   
          throw new DaoException("There is not the sessionFactory 
      }   
    }   
 
    public SessionFactory getSessionFactory() {   
      String sessionFactoryName = SpObserver.getSp();   
      return getSessionFactory(sessionFactoryName);   
    }   
 
    public void setSessionFactory(SessionFactory sessionFactory) {   
      this.sessionFactory = sessionFactory;   
    }   
 
    // SessionFactory介面需要實現的方法   
 
......   
 

MultiSessionFactory 的完整程式碼見我提供的附件。setSessionFactory()實際上是設定的預設sessionFactory,它在spring裝載的時候呼叫,其對應的資料來源應當是主資料來源,即專案初始化中需要讀取初始化資料的資料來源。在任何多資料來源專案中,都應當有一個存放初始化資料、系統維護資料、使用者許可權資料的資料來源,這就是主資料來源。因此MultiSessionFactory的配置應當這樣寫:

xml 程式碼
<bean id="sessionFactory" class="com.htxx.service.dao.MultiSessionFactory"> 
    <property name="sessionFactory"><ref bean="hostSessionFactory"/>property> 

SpServer的寫法與《如何在spring框架中解決多資料來源的問題》中的一樣,我就不再累贅了。

另外,在spring配置中配置多個數據源,每個資料來源對應一個sessionFactory,這個對應的sessionFactory中的值物件應當是該資料來源的值物件。客戶程式在執行資料訪問前,通過呼叫SpServer的putSp()方法,告訴MultiSessionFactory需要切換到哪個 sessionFactory,然後執行資料訪問。這樣,不同資料來源的值物件通過放在不同的sessionFactory中,避免了張冠李戴的情況。具體的示例見附件的MultiSessionFactoryTest。

另外的方案
也許有些朋友對以上方案還不滿意,因為在執行資料訪問前畢竟還要多做一步指定sessionFactory的工作。實際上,對於各個資料來源的資料結構不同的專案,一個值物件應當使用哪個資料來源有一個非常確定的對應關係。如果通過配置檔案將值物件與它的sessionFactory對應起來,那麼我們在執行資料訪問的時候傳遞的是哪個值物件,MultiSessionFactory馬上就可以去找到對應的sessionFactory。這個方案你可以通過AOP來製作一個攔截器攔截所有諸如save()、delete()、get()、load()等方法來實現,也可以擴充套件HibernateDaoSupport來實現。這樣的方案使客戶程式甚至都不用知道他是在操作的一個多資料來源系統。當然,這個方案感興趣的朋友可以自己去實現。

另外,在這個方案中的核心是運用 Decorator設計模式來解決切換sessionFactory的目的,即MultiSessionFactory的實現。至於通過什麼方式來通知 MultiSessionFactory應當切換到哪個SessionFactory,可以根據不同專案的情況自由選擇。我在這裡給大家提供了通過 SpOberver和建立值物件與sessionFactory關係的配置檔案這兩個方案,你也可以有自己的方案解決。

第三種情況的解決方案
前面我已經給出了第一種和第二種情況的解決方案:各個資料來源的資料結構不同的情況用MultiSessionFactory解決;各個資料來源的資料結構相同的情況用MultiDataSource解決。那麼第三種情況,各個資料來源的資料結構部分相同又有部分不同,又應當如何解決呢?當然是將 MultiSessionFactory和MultiDataSource結合起來解決。對於資料結構不同的部分,其分別建立各自的 sessionFactory然後通過MultiSessionFactory來切換,而對於資料結構相同的部分,建立共同的 sessionFactory和多個不同的dataSource然後通過MultiDataSource來切換就可以了。

還有的朋友問到這樣的方案其事務處理和二級快取的情況。這個方案是在spring框架下的解決方案,其事務處理的能力也是由spring的能力來決定的。目前 spring要處理跨資料庫的事務處理是通過JTA來實現的,這種方式在該方案中同樣可以實現,朋友們可以試一試。另外,本方案能使用二級快取嗎?當然可以。對於MultiSessionFactory當然沒有任何問題,它通過不同的sessionFactory分離開了不同的資料來源和值物件,我們可以毫無顧忌地使用。對於MultiDataSource來說,就有點問題了。MultiDataSource使多個數據源使用共同的 sessionFactory,因此它彷彿就是將多個數據源在邏輯上合併為一個數據源。正因為如此,我們需要保證對於同一個表在所有資料來源中都要主鍵唯一。什麼意思呢?資料來源A和資料來源B都有表T,如果資料來源A中的表T擁有ID為001的一條資料,那麼在資料來源B的表T中就不能有ID為001的記錄。如果你總是通過MultiDataSource來執行表的插入操作,並且使用uuid.hex生成主鍵,這當然不會有問題。但如果你有通過其它方式插入表的操作,你應當保證這樣的唯一性。另外,對於查詢的操作,快取中存放的既可能是資料來源A的資料,也可能是資料來源B的資料,因此你應當對資料有一個規劃。對於表T的資料,哪些應當插入到資料來源A中,哪些應當插入到B中,應當有一個定義。假如是通過不同單位來決定插入哪個資料來源,那麼在查詢資料來源A的表T是,應當增加條件只查詢資料來源A應當有的單位而排除調其它單位。如此這樣,你只要注意到這兩個問題,你就可以放心大膽地使用二級快取。