在我們的項目中遇到這樣一個問題:我們的項目需要連接多個數據庫,而且不同的客戶在每次訪問中根據需要會去訪問不同的數據庫。我們以往在 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,并實現了數據源動態切換:
?
?
- 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 :
?
- 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 。
?
- < bean ? id = "dataSource1" ??
- ???? class = "org.apache.commons.dbcp.BasicDataSource" > ??
- ???? < property ? name = "driverClassName" > ??
- ???????? < value > oracle.jdbc.driver.OracleDriver value > ??
- ???? property > ?
- ????...... ??
- bean > ??
- < bean ? id = "dataSource2" ??
- ???? class = "org.apache.commons.dbcp.BasicDataSource" > ??
- ???? < property ? name = "driverClassName" > ??
- ???????? < value > oracle.jdbc.driver.OracleDriver value > ?
- ???? property > ?? ?
- ????...... ??
- bean > ?? ?
為了得到 spring 的 ApplicationContext , MultiDataSource 類必須實現接口 org.springframework.context.ApplicationContextAware ,并且實現方法:
- 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 是一個不錯的設計:
?
- 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() :
?
- 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 中做如下配置:
?
- < 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框架中解決多數據源的問題
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
