在我們的項(xiàng)目中遇到這樣一個(gè)問(wèn)題:我們的項(xiàng)目需要連接多個(gè)數(shù)據(jù)庫(kù),而且不同的客戶(hù)在每次訪(fǎng)問(wèn)中根據(jù)需要會(huì)去訪(fǎng)問(wèn)不同的數(shù)據(jù)庫(kù)。我們以往在 spring 和 hibernate 框架中總是配置一個(gè)數(shù)據(jù)源,因而 sessionFactory 的 dataSource 屬性總是指向這個(gè)數(shù)據(jù)源并且恒定不變,所有 DAO 在使用 sessionFactory 的時(shí)候都是通過(guò)這個(gè)數(shù)據(jù)源訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)。但是現(xiàn)在,由于項(xiàng)目的需要,我們的 DAO 在訪(fǎng)問(wèn) sessionFactory 的時(shí)候都不得不在多個(gè)數(shù)據(jù)源中不斷切換,問(wèn)題就出現(xiàn)了:如何讓 sessionFactory 在執(zhí)行數(shù)據(jù)持久化的時(shí)候,根據(jù)客戶(hù)的需求能夠動(dòng)態(tài)切換不同的數(shù)據(jù)源?我們能不能在 spring 的框架下通過(guò)少量修改得到解決?是否有什么設(shè)計(jì)模式可以利用呢? ?
?
問(wèn)題的分析
我首先想到在 spring 的 applicationContext 中配置所有的 dataSource 。這些 dataSource 可能是各種不同類(lèi)型的,比如不同的數(shù)據(jù)庫(kù): Oracle 、 SQL Server 、 MySQL 等,也可能是不同的數(shù)據(jù)源:比如 apache 提供的 org.apache.commons.dbcp.BasicDataSource 、 spring 提供的 org.springframework.jndi.JndiObjectFactoryBean 等。然后 sessionFactory 根據(jù)客戶(hù)的每次請(qǐng)求,將 dataSource 屬性設(shè)置成不同的數(shù)據(jù)源,以到達(dá)切換數(shù)據(jù)源的目的。
?
但是,我很快發(fā)現(xiàn)一個(gè)問(wèn)題:當(dāng)多用戶(hù)同時(shí)并發(fā)訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)的時(shí)候會(huì)出現(xiàn)資源爭(zhēng)用的問(wèn)題。這都是“單例模式”惹的禍。眾所周知,我們?cè)谑褂? spring 框架的時(shí)候,在 beanFactory 中注冊(cè)的 bean 基本上都是采用單例模式,即 spring 在啟動(dòng)的時(shí)候,這些 bean 就裝載到內(nèi)存中,并且每個(gè) bean 在整個(gè)項(xiàng)目中只存在一個(gè)對(duì)象。正因?yàn)橹淮嬖谝粋€(gè)對(duì)象,對(duì)象的所有屬性,更準(zhǔn)確說(shuō)是實(shí)例變量,表現(xiàn)得就如同是個(gè)靜態(tài)變量(實(shí)際上“靜態(tài)”與“單例”往往是非常相似的兩個(gè)東西,我們常常用“靜態(tài)”來(lái)實(shí)現(xiàn)“單例”)。拿我們的問(wèn)題來(lái)說(shuō), sessionFactory 在整個(gè)項(xiàng)目中只有一個(gè)對(duì)象,它的實(shí)例變量 dataSource 也就只有一個(gè),就如同一個(gè)靜態(tài)變量一般。如果不同的用戶(hù)都不斷地去修改 dataSource 的值,必然會(huì)出現(xiàn)多用戶(hù)爭(zhēng)用一個(gè)變量的問(wèn)題,對(duì)系統(tǒng)產(chǎn)生隱患。
?
通過(guò)以上的分析,解決多數(shù)據(jù)源訪(fǎng)問(wèn)問(wèn)題的關(guān)鍵,就集中在 sessionFactory 在執(zhí)行數(shù)據(jù)持久化的時(shí)候,能夠通過(guò)某段代碼去根據(jù)客戶(hù)的需要?jiǎng)討B(tài)切換數(shù)據(jù)源,并解決資源爭(zhēng)用的問(wèn)題。
?
問(wèn)題的解決
-
采用 Decorator 設(shè)計(jì)模式
要解決這個(gè)問(wèn)題,我的思路鎖定在了這個(gè) dataSource 上了。如果 sessionFactory 指向的 dataSource 可以根據(jù)客戶(hù)的需求去連接客戶(hù)所需要的真正的數(shù)據(jù)源,即提供動(dòng)態(tài)切換數(shù)據(jù)源的功能,那么問(wèn)題就解決了。那么我們?cè)趺醋瞿??去修改那些我們要使用? dataSource 源碼嗎?這顯然不是一個(gè)好的方案,我們希望我們的修改與原 dataSource 代碼是分離的。根據(jù)以上的分析,使用 GoF 設(shè)計(jì)模式中的 Decorator 模式(裝飾者模式)應(yīng)當(dāng)是我們可以選擇的最佳方案。
?
什么是“ Decorator 模式”?簡(jiǎn)單點(diǎn)兒說(shuō)就是當(dāng)我們需要修改原有的功能,但我們又不愿直接去修改原有的代碼時(shí),設(shè)計(jì)一個(gè) Decorator 套在原有代碼外面。當(dāng)我們使用 Decorator 的時(shí)候與原類(lèi)完全一樣,當(dāng) Decorator 的某些功能卻已經(jīng)修改為了我們需要修改的功能。 Decorator 模式的結(jié)構(gòu)如圖。
?
?
我們本來(lái)需要修改圖中所有具體的 Component 類(lèi)的一些功能,但卻并不是去直接修改它們的代碼,而是在它們的外面增加一個(gè) Decorator 。 Decorator 與具體的 Component 類(lèi)都是繼承的 AbstractComponent ,因此它長(zhǎng)得和具體的 Component 類(lèi)一樣,也就是說(shuō)我們?cè)谑褂? Decorator 的時(shí)候就如同在使用 ConcreteComponentA 或者 ConcreteComponentB 一樣,甚至那些使用 ConcreteComponentA 或者 ConcreteComponentB 的客戶(hù)程序都不知道它們用的類(lèi)已經(jīng)改為了 Decorator ,但是 Decorator 已經(jīng)對(duì)具體的 Component 類(lèi)的部分方法進(jìn)行了修改,執(zhí)行這些方法的結(jié)果已經(jīng)不同了。
?
-
設(shè)計(jì) MultiDataSource 類(lèi)
現(xiàn)在回到我們的問(wèn)題,我們需要對(duì) dataSource 的功能進(jìn)行變更,但又不希望修改 dataSource 中的任何代碼。我這里指的 dataSource 是所有實(shí)現(xiàn) javax.sql.DataSource 接口的類(lèi),我們常用的包括 apache 提供的 org.apache.commons.dbcp.BasicDataSource 、 spring 提供的 org.springframework.jndi.JndiObjectFactoryBean 等,這些類(lèi)我們不可能修改它們本身,更不可能對(duì)它們一個(gè)個(gè)地修改以實(shí)現(xiàn)動(dòng)態(tài)分配數(shù)據(jù)源的功能,同時(shí),我們又希望使用 dataSource 的 sessionFactory 根本就感覺(jué)不到這樣的變化。 Decorator 模式就正是解決這個(gè)問(wèn)題的設(shè)計(jì)模式。
?
首先寫(xiě)一個(gè) Decorator 類(lèi),我取名叫 MultiDataSource,通過(guò)它來(lái)動(dòng)態(tài)切換數(shù)據(jù)源 。同時(shí)在配置文件中將sessionFactory的dataSource屬性由原來(lái)的某個(gè)具體的dataSource改為MultiDataSource。如圖:
?
?
對(duì)比原 Decorator 模式, AbstractComponent 是一個(gè)抽象類(lèi),但在這里我們可以將這個(gè)抽象類(lèi)用接口來(lái)代替,即 DataSource 接口,而 ConcreteComponent 就是那些 DataSource 的實(shí)現(xiàn)類(lèi),如 BasicDataSource 、 JndiObjectFactoryBean 等。 MultiDataSource 封裝了具體的dataSource,并實(shí)現(xiàn)了數(shù)據(jù)源動(dòng)態(tài)切換:
?
?
- 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接口應(yīng)當(dāng)實(shí)現(xiàn)的方法 ??
- ??
- ???? public ?DataSource?getDataSource(){ ??
- ???????? return ? this .dataSource; ??
- ????????} ??
- ????} ??
- ???? public ? void ?setDataSource(DataSource?dataSource)?{ ??
- ???????? this .dataSource?=?dataSource; ??
- ????} ??
- } ??
?
客戶(hù)在發(fā)出請(qǐng)求的時(shí)候,將dataSourceName放到request中,然后把request中的數(shù)據(jù)源名通過(guò)調(diào)用new MultiDataSource(dataSource) 時(shí)可以告訴 MultiDataSource 客戶(hù)需要的數(shù)據(jù)源,就可以實(shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)源了。但細(xì)心的朋友會(huì)發(fā)現(xiàn)這在單例的情況下就是問(wèn)題的,因?yàn)? MultiDataSource 在系統(tǒng)中只有一個(gè)對(duì)象,它的實(shí)例變量 dataSource 也只有一個(gè),就如同一個(gè)靜態(tài)變量一般。正因?yàn)槿绱耍? 單例模式讓許多設(shè)計(jì)模式都不得不需要更改,這將在我的《“單例”更改了我們的設(shè)計(jì)模式》中詳細(xì)討論。那么,我們?cè)趩卫J较氯绾卧O(shè)計(jì)呢?
?
-
單例模式下的 MultiDataSource
在單例模式下,由于我們?cè)诿看握{(diào)用 MultiDataSource 的方法的時(shí)候, dataSource 都可能是不同的,所以我們不能將 dataSource 放在實(shí)例變量 dataSource 中,最簡(jiǎn)單的方式就是在方法 getDataSource() 中增加參數(shù),告訴 MultiDataSource 我到底調(diào)用的是哪個(gè) 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?
- ????????} ??
- ????} ??
?
值得一提的是,我需要的數(shù)據(jù)源已經(jīng)都在 spring 的配置文件中注冊(cè), dataSourceName 就是其對(duì)應(yīng)的 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 類(lèi)必須實(shí)現(xiàn)接口 org.springframework.context.ApplicationContextAware ,并且實(shí)現(xiàn)方法:
- private ?ApplicationContext?applicationContext?=? null ; ??
- public ? void ?setApplicationContext(ApplicationContext?applicationContext)? throws ?BeansException?{ ??
- ???????? this .applicationContext?=?applicationContext; ??
- ????} ??
?
如此這樣,我就可以通過(guò) this . applicationContext .getBean(dataSourceName) 得到 dataSource 了。
?
-
通過(guò)線(xiàn)程傳遞 dataSourceName
查看以上設(shè)計(jì), MultiDataSource 依然無(wú)法運(yùn)行,因?yàn)橛脩?hù)在發(fā)出請(qǐng)求時(shí),他需要連接什么數(shù)據(jù)庫(kù),其數(shù)據(jù)源名是放在 request 中的,要將 request 中的數(shù)據(jù)源名傳給 MultiDataSource ,需要經(jīng)過(guò) BUS 和 DAO ,也就是說(shuō)為了把數(shù)據(jù)源名傳給 MultiDataSource , BUS 和 DAO 的所有方法都要增加 dataSourceName 的參數(shù),這是我們不愿看到的。寫(xiě)一個(gè)類(lèi),通過(guò)線(xiàn)程的方式跳過(guò) BUS 和 DAO 直接傳遞給 MultiDataSource 是一個(gè)不錯(cuò)的設(shè)計(jì):
?
- 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(); ??
- ????} ??
- } ??
?
做一個(gè) filter ,每次客戶(hù)發(fā)出請(qǐng)求的時(shí)候就調(diào)用 SpObserver. petSp ( dataSourceName ) ,將 request 中的 dataSourceName 傳遞給 SpObserver 對(duì)象。 最后修改 MultiDataSource 的方法 getDataSource() :
?
- public ?DataSource?getDataSource(){ ??
- ????????String?sp?=?SpObserver.getSp(); ??
- ???????? return ?getDataSource(sp); ??
- ????} ??
?
完整的 MultiDataSource 代碼在附件中。
?
-
動(dòng)態(tài)添加數(shù)據(jù)源
通過(guò)以上方案,我們解決了動(dòng)態(tài)分配數(shù)據(jù)源的問(wèn)題,但你可能提出疑問(wèn):方案中的數(shù)據(jù)源都是配置在 spring 的 ApplicationContext 中,如果我在程序運(yùn)行過(guò)程中動(dòng)態(tài)添加數(shù)據(jù)源怎么辦?這確實(shí)是一個(gè)問(wèn)題,而且在我們的項(xiàng)目中也確實(shí)遇到。 spring 的 ApplicationContext 是在項(xiàng)目啟動(dòng)的時(shí)候加載的。加載以后,我們?nèi)绾蝿?dòng)態(tài)地加載新的 bean 到 ApplicationContext 中呢?我想到如果用 spring 自己的方法解決這個(gè)問(wèn)題就好了。所幸的是,在查看 spring 的源代碼后,我找到了這樣的代碼,編寫(xiě)了 DynamicLoadBean 類(lèi),只要 調(diào)用 loadBean() 方法,就可以將某個(gè)或某幾個(gè)配置文件中的 bean 加載到 ApplicationContext 中(見(jiàn)附件)。不通過(guò)配置文件直接加載對(duì)象,在 spring 的源碼中也有,感興趣的朋友可以自己研究。
?
?
-
在 spring 中配置
在完成了所有這些設(shè)計(jì)以后,我最后再?lài)Z叨一句。我們應(yīng)當(dāng)在 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 屬性實(shí)際上更準(zhǔn)確地說(shuō)應(yīng)當(dāng)是 defaultDataSource ,即 spring 啟動(dòng)時(shí)以及在客戶(hù)沒(méi)有指定數(shù)據(jù)源時(shí)應(yīng)當(dāng)指定的默認(rèn)數(shù)據(jù)源。
?
該方案的優(yōu)勢(shì)
?
以上方案與其它方案相比,它有哪些優(yōu)勢(shì)呢?
?
首先,這個(gè)方案完全是在 spring 的框架下解決的,數(shù)據(jù)源依然配置在 spring 的配置文件中, sessionFactory 依然去配置它的 dataSource 屬性,它甚至都不知道 dataSource 的改變。唯一不同的是在真正的 dataSource 與 sessionFactory 之間增加了一個(gè) MultiDataSource 。
?
其次,實(shí)現(xiàn)簡(jiǎn)單,易于維護(hù)。這個(gè)方案雖然我說(shuō)了這么多東西,其實(shí)都是分析,真正需要我們寫(xiě)的代碼就只有 MultiDataSource 、 SpObserver 兩個(gè)類(lèi)。 MultiDataSource 類(lèi)真正要寫(xiě)的只有 getDataSource() 和 getDataSource(sp) 兩個(gè)方法,而 SpObserver 類(lèi)更簡(jiǎn)單了。實(shí)現(xiàn)越簡(jiǎn)單,出錯(cuò)的可能就越小,維護(hù)性就越高。
?
最后,這個(gè)方案可以使單數(shù)據(jù)源與多數(shù)據(jù)源兼容。這個(gè)方案完全不影響 BUS 和 DAO 的編寫(xiě)。如果我們的項(xiàng)目在開(kāi)始之初是單數(shù)據(jù)源的情況下開(kāi)發(fā),隨著項(xiàng)目的進(jìn)行,需要變更為多數(shù)據(jù)源,則只需要修改 spring 配置,并少量修改 MVC 層以便在請(qǐng)求中寫(xiě)入需要的數(shù)據(jù)源名,變更就完成了。如果我們的項(xiàng)目希望改回單數(shù)據(jù)源,則只需要簡(jiǎn)單修改配置文件。這樣,為我們的項(xiàng)目將增加更多的彈性。
?
特別說(shuō)明:實(shí)例中的DynamicLoadBean在web環(huán)境下運(yùn)行會(huì)出錯(cuò),需要將類(lèi)中AbstractApplicationContext改為org.springframework.context.ConfigurableApplicationContext。
?
相關(guān)博客: 再析在spring框架中解決多數(shù)據(jù)源的問(wèn)題
?
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元

