基于spring的aop实现多数据源动态切换 博客分类: javaspring 动态切换springaopAbstractRoutingDataSource
一、动态切换数据源理论知识
项目中我们经常会遇到多数据源的问题,尤其是数据同步或定时任务等项目更是如此;又例如:读写分离数据库配置的系统。
1、相信很多人都知道JDK代理,分静态代理和动态代理两种,同样的,多数据源设置也分为类似的两种:
1)静态数据源切换:
一般情况下,我们可以配置多个数据源,然后为每个数据源写一套对应的sessionFactory和dao层,我们称之为静态数据源配置,这样的好处是想调用那个数据源,直接调用dao层即可。但缺点也很明显,每个Dao层代码中写死了一个SessionFactory,这样日后如果再多一个数据源,还要改代码添加一个SessionFactory,显然这并不符合开闭原则。
2)动态数据源切换:
配置多个数据源,只对应一套sessionFactory,根据需要,数据源之间可以动态切换。
2、动态数据源切换时,如何保证数据库的事务:
目前事务最灵活的方式,是使用spring的声明式事务,本质是利用了spring的aop,在执行数据库操作前后,加上事务处理。
spring的事务管理,是基于数据源的,所以如果要实现动态数据源切换,而且在同一个数据源中保证事务是起作用的话,就需要注意二者的顺序问题,即:在事物起作用之前就要把数据源切换回来。
举一个例子:web开发常见是三层结构:controller、service、dao。一般事务都会在service层添加,如果使用spring的声明式事物管理,在调用service层代码之前,spring会通过aop的方式动态添加事务控制代码,所以如果要想保证事物是有效的,那么就必须在spring添加事务之前把数据源动态切换过来,也就是动态切换数据源的aop要至少在service上添加,而且要在spring声明式事物aop之前添加.根据上面分析:
最简单的方式是把动态切换数据源的aop加到controller层,这样在controller层里面就可以确定下来数据源了。不过,这样有一个缺点就是,每一个controller绑定了一个数据源,不灵活。对于这种:一个请求,需要使用两个以上数据源中的数据完成的业务时,就无法实现了。
针对上面的这种问题,可以考虑把动态切换数据源的aop放到service层,但要注意一定要在事务aop之前来完成。这样,对于一个需要多个数据源数据的请求,我们只需要在controller里面注入多个service实现即可。但这种做法的问题在于,controller层里面会涉及到一些不必要的业务代码,例如:合并两个数据源中的list…
此外,针对上面的问题,还可以再考虑一种方案,就是把事务控制到dao层,然后在service层里面动态切换数据源。
二、下面是我在实际项目中的一点应用:
1、首先,要有数据库的相关配置文件jdbc.properties:
#mysql cms.mysql.driver=com.mysql.jdbc.Driver cms.mysql.url=jdbc:mysql://127.0.0.1:3306/VSRecStream?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true cms.mysql.username=root cms.mysql.password=****** edition.mysql.driver=com.mysql.jdbc.Driver edition.mysql.url=jdbc:mysql://127.0.0.1:3306/ResourcePublish?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true edition.mysql.username=root edition.mysql.password=******
2、有了数据源,肯定需要将这些源管理起来,此时很多人肯定想到了spring,对的,看下面:
<!-- cms configuration --> <bean id="cmsBaseDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${cms.mysql.driver}" /> <property name="url" value="${cms.mysql.url}" /> <property name="username" value="${cms.mysql.username}" /> <property name="password" value="${cms.mysql.password}" /> <property name="maxActive" value="20" /> <property name="initialSize" value="5" /> <property name="maxWait" value="60000" /> <property name="minIdle" value="5" /> <property name="maxIdle" value="20" /> <property name="removeAbandoned" value="true" /> <property name="removeAbandonedTimeout" value="180"/> <property name="validationQuery" value="select 1" /> </bean> <!-- publish configuration --> <bean id="editionBaseDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${edition.mysql.driver}" /> <property name="url" value="${edition.mysql.url}" /> <property name="username" value="${edition.mysql.username}" /> <property name="password" value="${edition.mysql.password}" /> <property name="maxActive" value="20" /> <property name="initialSize" value="5" /> <property name="maxWait" value="60000" /> <property name="minIdle" value="5" /> <property name="maxIdle" value="20" /> <property name="removeAbandoned" value="true" /> <property name="removeAbandonedTimeout" value="180"/> <property name="validationQuery" value="select 1" /> </bean>
3、上面的数据源倒是配置起来了,但是怎么样才能实现一个sessionFactory来管理两个源呢,肯定是需要一个动态的代理类,写一个DynamicDataSource类继承 AbstractRoutingDataSource ,并实现 determineCurrentLookupKey方法即可,AbstractRoutingDataSource是spring里的一个实现类,有兴趣的朋友可以研究一下他的源码,在此,不做过多介绍。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 动态分派数据源 * @ClassName: DynamicDataSource * @author dove * @date 2017年3月21日 * */ public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return CustomerContextHolder.getCustomerType(); } }利用ThreadLocal解决线程安全问题
package com.visionvera.common; public class CustomerContextHolder { //用ThreadLocal来设置当前线程使用哪个dataSource private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); public static void setCustomerType(String customerType) { contextHolder.set(customerType); } public static String getCustomerType() { return contextHolder.get(); } public static void clearCustomerType() { contextHolder.remove(); } }
4、动态类编写完毕,就要用起来,实现一个sessionFactory管理多个数据源
<!--统一的dataSource--> <bean id="dynamicDataSource" class="com.visionvera.common.DynamicDataSource" > <property name="targetDataSources"> <map key-type="java.lang.String"> <!--通过不同的key决定用哪个dataSource--> <entry key="cmsBaseDataSource" value-ref="cmsBaseDataSource"></entry> <entry key="editionBaseDataSource" value-ref="editionBaseDataSource" ></entry> </map> </property> <!--设置默认的dataSource--> <property name="defaultTargetDataSource" ref="cmsBaseDataSource"></property> </bean>
<!-- define the SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dynamicDataSource" /> <property name="typeAliasesPackage" value="com.visionvera.cms.bean,com.visionvera.cms.vo,com.visionvera.edition.bean,com.visionvera.edition.vo"/> <!-- 自动扫描mapping.xml文件 --> <property name="mapperLocations" value="classpath:com/visionvera/*/dao/mapper/*.xml"/> </bean>
5、以上代码完成后,基本可以在每次调用service之前,通过手动切换数据源,即执行CustomerContextHolder.setCustomerType("cmsDataSource"),实现数据源的切换了,但是这样的话,完全达不到我们想要的动态切换数据源的需求。
那么我通过网上查找,发现有的朋友是通过写自定义注解配合拦截器来实现动态数据源切换的,但我个人感觉,如果在前期一直使用一个数据源的项目,后期突然要加入新的数据源的情况来说,不太适合,因为,这样的话,需要在每一个dao中添加注解,这样,之前的项目代码也需要修改,这也不是很好。而我的实现方法是,利用sping aop定义切面,在切面中实现数据源的切换,请看下面的代码:
编写切面代码:
package com.visionvera.common.aspect; import java.util.Map; import java.util.Map.Entry; import org.aspectj.lang.JoinPoint; import com.visionvera.common.CustomerContextHolder; /** * 动态切换数据源切面 * @ClassName: DataSourceAspect * @author chenting * @date 2017年3月22日 * */ public class DataSourceAspect{ private String defaultDataSource; private Map<String, Object> targetDataSources; public void doBefore(JoinPoint joinPoint) { boolean isSetDataSource = false; String targetName = joinPoint.getTarget().getClass().getName(); for(Entry<String, Object> entry : targetDataSources.entrySet()) { if(targetName.contains(entry.getKey())){ String value = entry.getValue().toString(); CustomerContextHolder.setCustomerType(value); isSetDataSource = true; break; } } if(!isSetDataSource) { CustomerContextHolder.setCustomerType(defaultDataSource); } } public void doAfterReturning(JoinPoint joinPoint) { CustomerContextHolder.clearCustomerType(); } public Map<String, Object> getTargetDataSources() { return targetDataSources; } public void setTargetDataSources(Map<String, Object> targetDataSources) { this.targetDataSources = targetDataSources; } public String getDefaultDataSource() { return defaultDataSource; } public void setDefaultDataSource(String defaultDataSource) { this.defaultDataSource = defaultDataSource; } }
在配置文件中利用spring aop进行管理
<!-- 配置数据源切换切面 --> <bean id="dataSourceChangeAspect" class="com.visionvera.common.aspect.DataSourceAspect"> <property name="defaultDataSource" value="cmsBaseDataSource"></property> <property name="targetDataSources"> <map key-type="java.lang.String"> <!--通过不同的key决定用哪个dataSource--> <entry key="com.visionvera.cms" value="cmsBaseDataSource"></entry> <entry key="com.visionvera.edition" value="editionBaseDataSource" ></entry> </map> </property> </bean>
上面map中配置的方式,主要是我仿照上面DynamicDataSource的模式来写的,DynamicDataSource是继续子父类的,而我这个是自己写的,当然想实现的功能是类似的。这样配置的目的是想实现,同一个包下的dao接口,使用同一个数据源,这样肯定也有局限性,在后期如果遇到的话,会进行优化。
<aop:config> <aop:pointcut id="serviceAop" expression="execution(* com.visionvera.*.service..*.*(..)))" /> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceAop" order="2"/> <!-- 配置数据源的动态切换 --> <aop:aspect ref="dataSourceChangeAspect" order="1"> <aop:before method="doBefore" pointcut-ref="serviceAop"/> <aop:after-returning method="doAfterReturning" pointcut-ref="serviceAop"/> </aop:aspect> </aop:config>
上面之所以跟事物控制放一块,是因为切换数据源跟事务的切入点相同,故此写在一起,当然完全可以分开写。
分开写的模式为:
<aop:config> <aop:aspect id="dataSourceAspect" ref="dataSourceChangeAspect"> <aop:pointcut id="daoAop" expression="execution(* com.visionvera.*.service..*.*(..)))" /> <aop:before method="doBefore" pointcut-ref="daoAop"/> <aop:after-returning method="doAfterReturning" pointcut-ref="daoAop"/> </aop:aspect> </aop:config>
至此,所有的配置均已完成。特别需要注意的一点是,order这一项,配置了执行的优先级,一定要在事务开启前,将数据源切换完毕。
但是还有一个比较大的问题没有解决,假如有两个数据源,此时需要同时添加一条数据,此时如果出现插入异常的话,事务是没法保证两个数据源都能回滚的,这个等待大神给指点迷津,谢谢。。