Spring(AbstractRoutingDataSource)實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)源切換示例
一、前言
近期一項(xiàng)目A需實(shí)現(xiàn)數(shù)據(jù)同步到另一項(xiàng)目B數(shù)據(jù)庫(kù)中,在不改變B項(xiàng)目的情況下,只好選擇項(xiàng)目A中切換數(shù)據(jù)源,直接把數(shù)據(jù)寫(xiě)入項(xiàng)目B的數(shù)據(jù)庫(kù)中。這種需求,在數(shù)據(jù)同步與定時(shí)任務(wù)中經(jīng)常需要。
那么問(wèn)題來(lái)了,該如何解決多數(shù)據(jù)源問(wèn)題呢?不光是要配置多個(gè)數(shù)據(jù)源,還得能靈活動(dòng)態(tài)的切換數(shù)據(jù)源。以spring+hibernate框架項(xiàng)目為例:
單個(gè)數(shù)據(jù)源綁定給sessionFactory,再在Dao層操作,若多個(gè)數(shù)據(jù)源的話,那不是就成了下圖:
可見(jiàn),sessionFactory都寫(xiě)死在了Dao層,若我再添加個(gè)數(shù)據(jù)源的話,則又得添加一個(gè)sessionFactory。所以比較好的做法應(yīng)該是下圖:
接下來(lái)就為大家講解下如何用spring來(lái)整合這些數(shù)據(jù)源,同樣以spring+hibernate配置為例。
二、實(shí)現(xiàn)原理
1、擴(kuò)展Spring的AbstractRoutingDataSource抽象類(該類充當(dāng)了DataSource的路由中介, 能有在運(yùn)行時(shí), 根據(jù)某種key值來(lái)動(dòng)態(tài)切換到真正的DataSource上。)
從AbstractRoutingDataSource的源碼中:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean
我們可以看到,它繼承了AbstractDataSource,而AbstractDataSource不就是javax.sql.DataSource的子類,So我們可以分析下它的getConnection方法:
public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } public Connection getConnection(String username, String password) throws SQLException { return determineTargetDataSource().getConnection(username, password); }
獲取連接的方法中,重點(diǎn)是determineTargetDataSource()方法,看源碼:
/** * Retrieve the current target DataSource. Determines the * {@link #determineCurrentLookupKey() current lookup key}, performs * a lookup in the {@link #setTargetDataSources targetDataSources} map, * falls back to the specified * {@link #setDefaultTargetDataSource default target DataSource} if necessary. * @see #determineCurrentLookupKey() */ protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; }
上面這段源碼的重點(diǎn)在于determineCurrentLookupKey()方法,這是AbstractRoutingDataSource類中的一個(gè)抽象方法,而它的返回值是你所要用的數(shù)據(jù)源dataSource的key值,有了這個(gè)key值,resolvedDataSource(這是個(gè)map,由配置文件中設(shè)置好后存入的)就從中取出對(duì)應(yīng)的DataSource,如果找不到,就用配置默認(rèn)的數(shù)據(jù)源。
看完源碼,應(yīng)該有點(diǎn)啟發(fā)了吧,沒(méi)錯(cuò)!你要擴(kuò)展AbstractRoutingDataSource類,并重寫(xiě)其中的determineCurrentLookupKey()方法,來(lái)實(shí)現(xiàn)數(shù)據(jù)源的切換:
package com.datasource.test.util.database; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 獲取數(shù)據(jù)源(依賴于spring) * @author linhy */ public class DynamicDataSource extends AbstractRoutingDataSource{ @Override protected Object determineCurrentLookupKey() { return DataSourceHolder.getDataSource(); } }
DataSourceHolder這個(gè)類則是我們自己封裝的對(duì)數(shù)據(jù)源進(jìn)行操作的類:
package com.datasource.test.util.database; /** * 數(shù)據(jù)源操作 * @author linhy */ public class DataSourceHolder { //線程本地環(huán)境 private static final ThreadLocal<String> dataSources = new ThreadLocal<String>(); //設(shè)置數(shù)據(jù)源 public static void setDataSource(String customerType) { dataSources.set(customerType); } //獲取數(shù)據(jù)源 public static String getDataSource() { return (String) dataSources.get(); } //清除數(shù)據(jù)源 public static void clearDataSource() { dataSources.remove(); } }
2、有人就要問(wèn),那你setDataSource這方法是要在什么時(shí)候執(zhí)行呢?當(dāng)然是在你需要切換數(shù)據(jù)源的時(shí)候執(zhí)行啦。手動(dòng)在代碼中調(diào)用寫(xiě)死嗎?這是多蠢的方法,當(dāng)然要讓它動(dòng)態(tài)咯。所以我們可以應(yīng)用spring aop來(lái)設(shè)置,把配置的數(shù)據(jù)源類型都設(shè)置成為注解標(biāo)簽,在service層中需要切換數(shù)據(jù)源的方法上,寫(xiě)上注解標(biāo)簽,調(diào)用相應(yīng)方法切換數(shù)據(jù)源咯(就跟你設(shè)置事務(wù)一樣):
@DataSource(name=DataSource.slave1) public List getProducts(){
當(dāng)然,注解標(biāo)簽的用法可能很少人用到,但它可是個(gè)好東西哦,大大的幫助了我們開(kāi)發(fā):
package com.datasource.test.util.database; import java.lang.annotation.*; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { String name() default DataSource.master; public static String master = "dataSource1"; public static String slave1 = "dataSource2"; public static String slave2 = "dataSource3"; }
三、配置文件
為了精簡(jiǎn)篇幅,省略了無(wú)關(guān)本內(nèi)容主題的配置。
項(xiàng)目中單獨(dú)分離出application-database.xml,關(guān)于數(shù)據(jù)源配置的文件。
<?xml version="1.0" encoding="UTF-8"?> <!-- Spring 數(shù)據(jù)庫(kù)相關(guān)配置 放在這里 --> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> <bean id = "dataSource1" class = "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"> <property name="url" value="${db1.url}"/> <property name = "user" value = "${db1.user}"/> <property name = "password" value = "${db1.pwd}"/> <property name="autoReconnect" value="true"/> <property name="useUnicode" value="true"/> <property name="characterEncoding" value="UTF-8"/> </bean> <bean id = "dataSource2" class = "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"> <property name="url" value="${db2.url}"/> <property name = "user" value = "${db2.user}"/> <property name = "password" value = "${db2.pwd}"/> <property name="autoReconnect" value="true"/> <property name="useUnicode" value="true"/> <property name="characterEncoding" value="UTF-8"/> </bean> <bean id = "dataSource3" class = "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"> <property name="url" value="${db3.url}"/> <property name = "user" value = "${db3.user}"/> <property name = "password" value = "${db3.pwd}"/> <property name="autoReconnect" value="true"/> <property name="useUnicode" value="true"/> <property name="characterEncoding" value="UTF-8"/> </bean> <!-- 配置多數(shù)據(jù)源映射關(guān)系 --> <bean id="dataSource" class="com.datasource.test.util.database.DynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <entry key="dataSource1" value-ref="dataSource1"></entry> <entry key="dataSource2" value-ref="dataSource2"></entry> <entry key="dataSource3" value-ref="dataSource3"></entry> </map> </property> <!-- 默認(rèn)目標(biāo)數(shù)據(jù)源為你主庫(kù)數(shù)據(jù)源 --> <property name="defaultTargetDataSource" ref="dataSource1"/> </bean> <bean id="sessionFactoryHibernate" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">com.datasource.test.util.database.ExtendedMySQLDialect</prop> <prop key="hibernate.show_sql">${SHOWSQL}</prop> <prop key="hibernate.format_sql">${SHOWSQL}</prop> <prop key="query.factory_class">org.hibernate.hql.classic.ClassicQueryTranslatorFactory</prop> <prop key="hibernate.connection.provider_class">org.hibernate.connection.C3P0ConnectionProvider</prop> <prop key="hibernate.c3p0.max_size">30</prop> <prop key="hibernate.c3p0.min_size">5</prop> <prop key="hibernate.c3p0.timeout">120</prop> <prop key="hibernate.c3p0.idle_test_period">120</prop> <prop key="hibernate.c3p0.acquire_increment">2</prop> <prop key="hibernate.c3p0.validate">true</prop> <prop key="hibernate.c3p0.max_statements">100</prop> </props> </property> </bean> <bean id="hibernateTemplate" class="org.springframework.orm.hibernate3.HibernateTemplate"> <property name="sessionFactory" ref="sessionFactoryHibernate"/> </bean> <bean id="dataSourceExchange" class="com.datasource.test.util.database.DataSourceExchange"/> <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactoryHibernate"/> </bean> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="insert*" propagation="NESTED" rollback-for="Exception"/> <tx:method name="add*" propagation="NESTED" rollback-for="Exception"/> <tx:method name="update*" propagation="NESTED" rollback-for="Exception"/> <tx:method name="modify*" propagation="NESTED" rollback-for="Exception"/> <tx:method name="edit*" propagation="NESTED" rollback-for="Exception"/> <tx:method name="del*" propagation="NESTED" rollback-for="Exception"/> <tx:method name="save*" propagation="NESTED" rollback-for="Exception"/> <tx:method name="send*" propagation="NESTED" rollback-for="Exception"/> <tx:method name="get*" read-only="true"/> <tx:method name="find*" read-only="true"/> <tx:method name="query*" read-only="true"/> <tx:method name="search*" read-only="true"/> <tx:method name="select*" read-only="true"/> <tx:method name="count*" read-only="true"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="service" expression="execution(* com.datasource..*.service.*.*(..))"/> <!-- 關(guān)鍵配置,切換數(shù)據(jù)源一定要比持久層代碼更先執(zhí)行(事務(wù)也算持久層代碼) --> <aop:advisor advice-ref="txAdvice" pointcut-ref="service" order="2"/> <aop:advisor advice-ref="dataSourceExchange" pointcut-ref="service" order="1"/> </aop:config> </beans>
四、疑問(wèn)
多數(shù)據(jù)源切換是成功了,但牽涉到事務(wù)呢?單數(shù)據(jù)源事務(wù)是ok的,但如果多數(shù)據(jù)源需要同時(shí)使用一個(gè)事務(wù)呢?這個(gè)問(wèn)題有點(diǎn)頭大,網(wǎng)絡(luò)上有人提出用atomikos開(kāi)源項(xiàng)目實(shí)現(xiàn)JTA分布式事務(wù)處理。你怎么看?
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Spring AbstractRoutingDatasource 動(dòng)態(tài)數(shù)據(jù)源的實(shí)例講解
- 淺談利用Spring的AbstractRoutingDataSource解決多數(shù)據(jù)源的問(wèn)題
- 詳解利用Spring的AbstractRoutingDataSource解決多數(shù)據(jù)源的問(wèn)題
- 使用Spring的AbstractRoutingDataSource實(shí)現(xiàn)多數(shù)據(jù)源切換示例
- ???????Spring多租戶數(shù)據(jù)源管理 AbstractRoutingDataSource
相關(guān)文章
Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之二手書(shū)商城系統(tǒng)的實(shí)現(xiàn)
這是一個(gè)使用了java+JSP+Springboot+maven+mysql+ThymeLeaf+FTP開(kāi)發(fā)的二手書(shū)商城系統(tǒng),是一個(gè)畢業(yè)設(shè)計(jì)的實(shí)戰(zhàn)練習(xí),具有在線書(shū)城該有的所有功能,感興趣的朋友快來(lái)看看吧2022-01-01java字符轉(zhuǎn)碼的三種方法總結(jié)及實(shí)例
這篇文章主要介紹了 java字符轉(zhuǎn)碼的三種方法總結(jié)及實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-03-03Java Web項(xiàng)目中實(shí)現(xiàn)文件下載功能的實(shí)例教程
這篇文章主要介紹了Java Web項(xiàng)目中實(shí)現(xiàn)文件下載功能的實(shí)例教程,分別講解了通過(guò)超鏈接實(shí)現(xiàn)下載以及通過(guò)Servlet程序?qū)崿F(xiàn)下載的方式,需要的朋友可以參考下2016-05-05java中快速創(chuàng)建帶初始值的List和Map實(shí)例
下面小編就為大家?guī)?lái)一篇java中快速創(chuàng)建帶初始值的List和Map實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10Intellij IDEA 旗艦版創(chuàng)建 Spring MVC 項(xiàng)目踩過(guò)的坑
IDEA旗艦版可以直接創(chuàng)建Spring MVC項(xiàng)目,但創(chuàng)建后的項(xiàng)目并不是直接就可以運(yùn)行,還需要進(jìn)行一些配置。這篇文章主要介紹了Intellij IDEA 旗艦版創(chuàng)建 Spring MVC 項(xiàng)目踩坑記 ,需要的朋友可以參考下2020-03-03Spring boot動(dòng)態(tài)修改日志級(jí)別的方法
我們經(jīng)常會(huì)遇到業(yè)務(wù)想看debug日志的問(wèn)題,但是debug日志頻繁打印會(huì)對(duì)日志查看有影響,且日志多對(duì)系統(tǒng)也會(huì)有一定的壓力,因此,如果可以在需要的時(shí)候動(dòng)態(tài)臨時(shí)調(diào)整下日志的級(jí)別則是比較完美的,spring boot已經(jīng)支持這種功能,需要的朋友可以參考下2022-12-12spring?cloud?gateway中netty線程池小優(yōu)化
這篇文章主要介紹了spring?cloud?gateway中netty線程池小優(yōu)化技巧示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07