【原】通过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 }
(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