【環境參數】
1、開發框架:Spring + SpringMVC + MyBatis
2、數據庫A的URL:jdbc.url=jdbc:mysql://172.16.17.164:3306/ test?characterEncoding=UTF-8&useUnicode=TRUE&autoReconnect=true&failOverReadOnly=false
3、數據庫B的URL:bakdb.jdbc.url=jdbc:mysql://172.16.17.68:3306/bakDB?characterEncoding=UTF-8&useUnicode=TRUE&autoReconnect=true&failOverReadOnly=false
【需求描述】
(1)當用戶調用X方法“之前”,系統會首先切換當前數據源為A數據源(bakDb數據庫),之后再去調用方法X。
(2)當用戶調用Y方法“之前”,系統會首先切換當前的數據源為B數據源(testDb數據庫),之后再去調用方法Y。
(3)X方法和Y方法所在的包名
X方法:該方法位於com.zjrodger.bakdata.service包下其子包下。
Y方法:該方法位於com.zjrodger.datatobank.service或者com.zjrodger.zxtobank.service包及其子包下。
【具體步驟】
1、編寫動態數據源相關代碼。
(1) 編寫DynamicDataSource類。
DynamicDataSource的主要作用是以Map<Object, Object>的形式,來存儲多個數據源。
因為該類繼承了父類AbstractRoutingDataSource,在父類中,多數據源的實例是被存放在一個名為“targetDataSource”的Map類型的成員變量中。

1 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; 2 3 public class DynamicDataSource extends AbstractRoutingDataSource { 4 5 @Override 6 protected Object determineCurrentLookupKey() { 7 return DatabaseContextHolder.getDbType(); 8 } 9 }
(2) 編寫DatabaseContextHolder類。

1 public class DatabaseContextHolder { 2 3 private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); 4 5 public static void setDbType(String dataSourceType) { 6 contextHolder.set(dataSourceType); 7 } 8 9 public static String getDbType() { 10 return contextHolder.get(); 11 } 12 13 public static void clearDbType() { 14 contextHolder.remove(); 15 } 16 }
2、編寫切換數據源的攔截器。

1 public class DataSourceInterceptor { 2 3 /** 數據源切換常量 */ 4 public static final String DATASOURCE_TEST_DB="dataSourceKey4TestDb"; 5 public static final String DATASOURCE_BAK_DB="dataSourceKey4BakDb"; 6 7 /** 8 * 設置數據源為test數據庫所對應的數據源。 9 * @param jp 10 */ 11 public void setdataSourceTestDb(JoinPoint jp) { 12 DatabaseContextHolder.setDbType(DATASOURCE_TEST_DB); 13 } 14 15 /** 16 * 設置數據源為bak數據庫所對應的數據源。 17 * @param jp 18 */ 19 public void setdataSourceBakDb(JoinPoint jp) { 20 DatabaseContextHolder.setDbType(DATASOURCE_BAK_DB); 21 } 22 }
3、在Spring配置文件中進行相關配置。
(1)配置兩個數據源
A.第一個數據源:

1 <bean id="c3p0DataSource4BakDb" class="com.mchange.v2.c3p0.ComboPooledDataSource" 2 destroy-method="close" depends-on="propertyConfigurer"> 3 <property name="driverClass" value="${bakdb.jdbc.driverclass}" /> 4 <property name="jdbcUrl" value="${bakdb.jdbc.url}" /> 5 <property name="user" value="${bakdb.jdbc.username}" /> 6 <property name="password" value="${bakdb.jdbc.password}" /> 7 8 <!-- 初始化時獲取的連接數,取值應在minPoolSize與maxPoolSize之間。Default: 3 --> 9 <property name="initialPoolSize" value="10" /> 10 <!-- 連接池中保留的最小連接數。 --> 11 <property name="minPoolSize" value="5" /> 12 <!-- 連接池中保留的最大連接數。Default: 15 --> 13 <property name="maxPoolSize" value="100" /> 14 <!-- 當連接池中的連接耗盡的時候c3p0一次同時獲取的連接數。Default: 3 --> 15 <property name="acquireIncrement" value="5" /> 16 <!-- 最大空閑時間,10秒內未使用則連接被丟棄。若為0則永不丟棄。Default: 0 --> 17 <property name="maxIdleTime" value="10" /> 18 <!-- JDBC的標准參數,用以控制數據源內加載的PreparedStatements數量。但由於預緩存的statements 屬於單個connection而不是整個連接池。所以設置這個參數需要考慮到多方面的因素。 19 如果maxStatements與maxStatementsPerConnection均為0,則緩存被關閉。Default: 0 --> 20 <property name="maxStatements" value="0" /> 21 <!-- 連接池用完時客戶調用getConnection()后等待獲取連接的時間,單位:毫秒。超時后會拋出 SQLEXCEPTION,如果設置0,則無限等待。Default:0 --> 22 <property name="checkoutTimeout" value="30000" /> 23 </bean>
B.第二個數據源:

1 <bean id="c3p0DataSource4TestDb" class="com.mchange.v2.c3p0.ComboPooledDataSource" 2 destroy-method="close" depends-on="propertyConfigurer"> 3 <property name="driverClass" value="${jdbc.driverclass}" /> 4 <property name="jdbcUrl" value="${jdbc.url}" /> 5 <property name="user" value="${jdbc.username}" /> 6 <property name="password" value="${jdbc.password}" /> 7 8 <!-- 初始化時獲取的連接數,取值應在minPoolSize與maxPoolSize之間。Default: 3 --> 9 <property name="initialPoolSize" value="10" /> 10 <!-- 連接池中保留的最小連接數。 --> 11 <property name="minPoolSize" value="5" /> 12 <!-- 連接池中保留的最大連接數。Default: 15 --> 13 <property name="maxPoolSize" value="100" /> 14 <!-- 當連接池中的連接耗盡的時候c3p0一次同時獲取的連接數。Default: 3 --> 15 <property name="acquireIncrement" value="5" /> 16 <!-- 最大空閑時間,10秒內未使用則連接被丟棄。若為0則永不丟棄。Default: 0 --> 17 <property name="maxIdleTime" value="10" /> 18 <!-- JDBC的標准參數,用以控制數據源內加載的PreparedStatements數量。但由於預緩存的statements 屬於單個connection而不是整個連接池。所以設置這個參數需要考慮到多方面的因素。 19 如果maxStatements與maxStatementsPerConnection均為0,則緩存被關閉。Default: 0 --> 20 <property name="maxStatements" value="0" /> 21 <!-- 連接池用完時客戶調用getConnection()后等待獲取連接的時間,單位:毫秒。超時后會拋出 SQLEXCEPTION,如果設置0,則無限等待。Default:0 --> 22 <property name="checkoutTimeout" value="30000" /> 23 </bean>
(2)兩個數據源所對應的properties屬性文件

1 # =========== Test數據庫相關信息 ============ 2 jdbc.url=jdbc:mysql://172.16.17.164:3306/ test?characterEncoding=UTF-8&useUnicode=TRUE&autoReconnect=true&failOverReadOnly=false 3 jdbc.username=root 4 jdbc.password=123456 5 jdbc.driverclass=com.mysql.jdbc.Driver 6 jdbc.ip=172.16.5.64 7 jdbc.dbname=test 8 9 10 # =========== BakDB數據庫相關信息 ============ 11 bakdb.jdbc.url=jdbc:mysql://172.16.17.68:3306/bakDB?characterEncoding=UTF-8&useUnicode=TRUE&autoReconnect=true&failOverReadOnly=false 12 bakdb.jdbc.username=root 13 bakdb.jdbc.password=123456 14 bakdb.jdbc.driverclass=com.mysql.jdbc.Driver 15 bakdb.jdbc.ip=172.16.17.68 16 bakdb.jdbc.dbname=bakDB
(3)配置DynamicDataSource這個Bean(關鍵)。
該DynamicDataSource的主要作用是以Map<Object, Object>的形式,來存儲多個數據源。

1 <!-- 配置可以存儲多個數據源的Bean --> 2 <bean id="dataSource" class="com.beebank.pub.datasource.DynamicDataSource"> 3 <property name="targetDataSources"> 4 <map key-type="java.lang.String"> 5 <entry key="dataSourceKey4TestDb" value-ref="c3p0DataSource4TestDb" /> 6 <entry key="dataSourceKey4BakDb" value-ref="c3p0DataSource4BakDb" /> 7 </map> 8 </property> 9 <property name="defaultTargetDataSource" ref="c3p0DataSource4HuihangDb" /> 10 </bean>
(4)配置DataSourceInterceptor這個Bean(關鍵)。

1 <!-- 配置切換數據源Key的攔截器 --> 2 <bean id="dataSourceInterceptor" class="com.zjrodger.pub.datasource.DataSourceInterceptor"></bean>
(5)利用AOP,配置控制數據源在特定條件下切換的切面(關鍵,重要)。
注意要添加aop名字空間。

1 <!-- 1.配置Spring框架自身提供的切面類 --> 2 <tx:advice id="userTxAdvice" transaction-manager="transactionManager"> 3 <tx:attributes> 4 <tx:method name="delete*" propagation="REQUIRED" read-only="false" 5 rollback-for="java.lang.Exception" no-rollback-for="java.lang.RuntimeException" /> 6 <tx:method name="insert*" propagation="REQUIRED" read-only="false" 7 rollback-for="java.lang.Exception" /> 8 <tx:method name="update*" propagation="REQUIRED" read-only="false" 9 rollback-for="java.lang.Exception" /> 10 <tx:method name="find*" propagation="SUPPORTS" /> 11 <tx:method name="get*" propagation="SUPPORTS" /> 12 <tx:method name="select*" propagation="SUPPORTS" /> 13 </tx:attributes> 14 </tx:advice> 15 16 <!-- 2.配置用戶自定義的切面,用於切換數據源Key --> 17 <bean id="dataSourceInterceptor" class="com.zjrodger.pub.datasource.DataSourceInterceptor"></bean> 18 19 <!-- 3.(重要)配置Spring事務切面和自定義切面類,動態切換數據源,注意兩切面的執行順序 --> 20 <aop:config> 21 <!-- (1) Spring框架自身提供的切面 --> 22 <aop:advisor advice-ref="userTxAdvice" pointcut="execution(public * com.zjrodger.*.service..*.*(..))" order="2"/> 23 24 <!-- (2) 用戶自定義的切面,根據切入點,動態切換數據源。 --> 25 <aop:aspect id="dataSourceAspect" ref="dataSourceInterceptor" order="1"> 26 <aop:before method="setdataSourceBakDb" pointcut="execution(* com.zjrodger.bakdata.service..*.*(..))"/> 27 <aop:before method="setdataSourceTestDb" pointcut="execution(* com.zjrodger.datatobank.service..*.*(..))"/> 28 </aop:aspect> 29 </aop:config>
注意:
A.注意上述兩個切面中的order屬性的配置。
B.自定義切面類和Spring自帶事務切面類(即<aop:advisor>元素)的執行的先后順序要配置正確,否則就會導致導致數據源不能動態切換。
在AOP中,當執行同一個切入點時,不同切面的執行先后順序是由“每個切面的order屬性”而定的,order越小,則該該切面中的通知越先被執行。
上述<aop:config>元素中,引用了兩個切面類:“userTxAdvice類”和“dataSourceAspect類”,其中<aop:advisor>是Spring框架自定義的切面標簽。
根據兩個切面類order屬性的定義,當程序執行時並且觸發切入點后(即調用com.zjrodger.bakdata.service包及其子包下的方法),dataSourceAspect切面類中的setdatasourceBakDb()方通知法首先執行,之后才會執行userTxAdvice事務類中的相關通知方。
說明
切面類“DataSourceInterceptor”中有兩個方法:setdataSourceTestDb()方法和setdataSourceBakDb()。
1)當用戶調用“com.zjrodger.bakdata.service”包及其子包下的方法X“之前”,系統會首先去調用setdataSourceBakDb()方法,設置當前數據源為bakDb的數據源,之后再去調用方法X。
2)當用戶調用“com.zjrodger.datatobank.service”或者“com.zjrodger.zxtobank.service”包及其子包下的方法Y之前,系統會首先去調調用setdataSourceTestDb()方法,設置當前的數據源為testDb數據庫的數據源,之后再去調用方法Y。
(6)完整的Spring配置文檔

1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" 4 xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" 5 xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" 6 xmlns:task="http://www.springframework.org/schema/task" 7 xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd 8 http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd 9 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 10 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd 11 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd 12 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> 13 14 <!-- 該配置為自動掃描配置的包下所有使用@Controller注解的類 --> 15 <context:component-scan base-package="com.zjrodger" /> 16 17 <bean id="propertyConfigurer" 18 class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> 19 <property name="location"> 20 <value>classpath:properties/dbconfig.properties</value> 21 </property> 22 <property name="fileEncoding" value="utf-8" /> 23 </bean> 24 25 <!-- 備份庫數據庫數據源 --> 26 <bean id="c3p0DataSource4BakDb" class="com.mchange.v2.c3p0.ComboPooledDataSource" 27 destroy-method="close" depends-on="propertyConfigurer"> 28 <property name="driverClass" value="${bakdb.jdbc.driverclass}" /> 29 <property name="jdbcUrl" value="${bakdb.jdbc.url}" /> 30 <property name="user" value="${bakdb.jdbc.username}" /> 31 <property name="password" value="${bakdb.jdbc.password}" /> 32 33 <!-- 初始化時獲取的連接數,取值應在minPoolSize與maxPoolSize之間。Default: 3 --> 34 <property name="initialPoolSize" value="10" /> 35 <!-- 連接池中保留的最小連接數。 --> 36 <property name="minPoolSize" value="5" /> 37 <!-- 連接池中保留的最大連接數。Default: 15 --> 38 <property name="maxPoolSize" value="100" /> 39 <!-- 當連接池中的連接耗盡的時候c3p0一次同時獲取的連接數。Default: 3 --> 40 <property name="acquireIncrement" value="5" /> 41 <!-- 最大空閑時間,10秒內未使用則連接被丟棄。若為0則永不丟棄。Default: 0 --> 42 <property name="maxIdleTime" value="10" /> 43 <!-- JDBC的標准參數,用以控制數據源內加載的PreparedStatements數量。但由於預緩存的statements 屬於單個connection而不是整個連接池。所以設置這個參數需要考慮到多方面的因素。 44 如果maxStatements與maxStatementsPerConnection均為0,則緩存被關閉。Default: 0 --> 45 <property name="maxStatements" value="0" /> 46 <!-- 連接池用完時客戶調用getConnection()后等待獲取連接的時間,單位:毫秒。超時后會拋出 SQLEXCEPTION,如果設置0,則無限等待。Default:0 --> 47 <property name="checkoutTimeout" value="30000" /> 48 </bean> 49 50 51 <!--Test數據庫數據源 --> 52 <bean id="c3p0DataSource4TestDb" class="com.mchange.v2.c3p0.ComboPooledDataSource" 53 destroy-method="close" depends-on="propertyConfigurer"> 54 <property name="driverClass" value="${jdbc.driverclass}" /> 55 <property name="jdbcUrl" value="${jdbc.url}" /> 56 <property name="user" value="${jdbc.username}" /> 57 <property name="password" value="${jdbc.password}" /> 58 59 <!-- 初始化時獲取的連接數,取值應在minPoolSize與maxPoolSize之間。Default: 3 --> 60 <property name="initialPoolSize" value="10" /> 61 <!-- 連接池中保留的最小連接數。 --> 62 <property name="minPoolSize" value="5" /> 63 <!-- 連接池中保留的最大連接數。Default: 15 --> 64 <property name="maxPoolSize" value="100" /> 65 <!-- 當連接池中的連接耗盡的時候c3p0一次同時獲取的連接數。Default: 3 --> 66 <property name="acquireIncrement" value="5" /> 67 <!-- 最大空閑時間,10秒內未使用則連接被丟棄。若為0則永不丟棄。Default: 0 --> 68 <property name="maxIdleTime" value="10" /> 69 <!-- JDBC的標准參數,用以控制數據源內加載的PreparedStatements數量。但由於預緩存的statements 屬於單個connection而不是整個連接池。所以設置這個參數需要考慮到多方面的因素。 70 如果maxStatements與maxStatementsPerConnection均為0,則緩存被關閉。Default: 0 --> 71 <property name="maxStatements" value="0" /> 72 <!-- 連接池用完時客戶調用getConnection()后等待獲取連接的時間,單位:毫秒。超時后會拋出 SQLEXCEPTION,如果設置0,則無限等待。Default:0 --> 73 <property name="checkoutTimeout" value="30000" /> 74 </bean> 75 76 <!-- 配置可以存儲多個數據源的Bean --> 77 <bean id="dataSource" class="com.zjrodger.pub.datasource.DynamicDataSource"> 78 <property name="targetDataSources"> 79 <map key-type="java.lang.String"> 80 <entry key="dataSourceKey4TestDb" value-ref="c3p0DataSource4TestDb" /> 81 <entry key="dataSourceKey4BakDb" value-ref="c3p0DataSource4BakDb" /> 82 </map> 83 </property> 84 <property name="defaultTargetDataSource" ref="c3p0DataSource4TestDb" /> 85 </bean> 86 87 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> 88 <!-- <property name="dataSource" ref="c3p0DataSource" /> --> 89 <property name="dataSource" ref="dataSource" /> 90 91 <property name="mapperLocations" value="classpath*:com/zjrodger/**/dao/xml/*.xml" /> 92 <!-- 添加分頁插件 --> 93 <property name="plugins"> 94 <list> 95 <bean class="com.github.pagehelper.PageHelper"> 96 <property name="properties"> 97 <props> 98 <prop key="dialect">mysql</prop> 99 <prop key="offsetAsPageNum">true</prop> 100 <prop key="rowBoundsWithCount">true</prop> 101 <prop key="pageSizeZero">true</prop> 102 <prop key="reasonable">true</prop> 103 </props> 104 </property> 105 </bean> 106 </list> 107 </property> 108 109 </bean> 110 111 <bean id="transactionManager" 112 class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 113 <!-- <property name="dataSource" ref="c3p0DataSource" /> --> 114 <property name="dataSource" ref="dataSource" /> 115 </bean> 116 117 <!-- 注解驅動,使spring的controller全部生效 --> 118 <mvc:annotation-driven /> 119 <!-- 注解驅動,是spring的task全部生效 --> 120 <task:annotation-driven /> 121 122 123 <aop:aspectj-autoproxy expose-proxy="true" /> 124 <tx:annotation-driven transaction-manager="transactionManager"/> 125 126 <!-- Spring聲明式事務切面 --> 127 <tx:advice id="userTxAdvice" transaction-manager="transactionManager"> 128 <tx:attributes> 129 <tx:method name="delete*" propagation="REQUIRED" read-only="false" 130 rollback-for="java.lang.Exception" no-rollback-for="java.lang.RuntimeException" /> 131 <tx:method name="insert*" propagation="REQUIRED" read-only="false" 132 rollback-for="java.lang.Exception" /> 133 <tx:method name="update*" propagation="REQUIRED" read-only="false" 134 rollback-for="java.lang.Exception" /> 135 <tx:method name="find*" propagation="SUPPORTS" /> 136 <tx:method name="get*" propagation="SUPPORTS" /> 137 <tx:method name="select*" propagation="SUPPORTS" /> 138 </tx:attributes> 139 </tx:advice> 140 141 <aop:config> 142 <!-- Spring框架自身提供的切面 --> 143 <aop:advisor advice-ref="userTxAdvice" pointcut="execution(public * com.zjrodger.*.service..*.*(..))" order="2"/> 144 145 <!-- 用戶自定義的切面,根據切入點,動態切換數據源。 --> 146 <aop:aspect id="dataSourceAspect" ref="dataSourceInterceptor" order="1"> 147 <aop:before method="setdataSourceBakDb" pointcut="execution(* com.zjrodger.bakdata.service..*.*(..))"/> 148 <aop:before method="setdataSourceTestDb" pointcut="execution(* com.zjrodger.datatobank.service..*.*(..))"/> 149 <aop:before method="setdataSourceTestDb" pointcut="execution(* com.zjrodger.zxtobank.service..*.*(..))"/> 150 </aop:aspect> 151 </aop:config> 152 153 <!-- 配置切換數據源Key的攔截器 --> 154 <bean id="dataSourceInterceptor" class="com.zjrodger.pub.datasource.DataSourceInterceptor"></bean> 155 156 <!-- mybatis配置 --> 157 <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> 158 <property name="basePackage" value="com.zjrodger.pub.dao,com.zjrodger.zxtobank.dao,com.zjrodger.bakdata.dao" /> 159 <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> 160 </bean> 161 </beans>
至此,MyBatis多數據源的配置完畢,之后在自己的環境下進行測試,結果測試通過。
要特別注意自定義AOP切面與Spring自帶的事務切面的執行順序,即注意<aop:config>中的配置部分,否則,很容易會出現動態切換數據源失敗的現象。
【同專題博客內連接】
1、Order屬性決定了不同切面類中通知執行的先后順序
http://www.cnblogs.com/zjrodger/p/5633922.html
2、不定義Order屬性,通過切面類的定義順序來決定通知執行的先后順序
http://www.cnblogs.com/zjrodger/p/5633951.html
【其他參考連接】
1、《Spring中事務與aop的先后順序問題》http://my.oschina.net/HuifengWang/blog/304188