【原】通過AOP實現MyBatis多數據源的動態切換


【環境參數】
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 }
DynamicDataSource

(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 }
DatabaseContextHolder

 

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 }
DataSourceInterceptor

 

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>
BakDb數據庫的數據源

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>
TestDb數據庫的數據源

(2)兩個數據源所對應的properties屬性文件

 1 # =========== Test數據庫相關信息 ============
 2 jdbc.url=jdbc:mysql://172.16.17.164:3306/ test?characterEncoding=UTF-8&amp;useUnicode=TRUE&amp;autoReconnect=true&amp;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&amp;useUnicode=TRUE&amp;autoReconnect=true&amp;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>        
配置dataSource這個Bean

(4)配置DataSourceInterceptor這個Bean(關鍵)。

1 <!-- 配置切換數據源Key的攔截器 -->
2 <bean id="dataSourceInterceptor" class="com.zjrodger.pub.datasource.DataSourceInterceptor"></bean>
Bean—dataSourceInterceptor

(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>
(重要)配置Spring事務切面和自定義切面類,動態切換數據源,注意兩切面的執行順序

注意:

    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>
applicationContext.xml

至此,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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM