Spring AOP实现注解式的Mybatis多数据源切换

mapleJia 2020-11-08 11:21:18
mybatis spring 实现 AOP 注解


一、为什么要使用多数据源切换?
多数据源切换是为了满足什么业务场景?正常情况下,一个微服务或者说一个WEB项目,在使用Mybatis作为数据库链接和操作框架的情况下通常只需要构建一个系统库,在该系统库创建业务表来满足需求,当然也有分为测试库和正式库dev/prod,不过这俩库的切换是使用配置文件进行切分的,在项目启动时或者打成maven JAR包指定environment-dev.properties或者environment-prod.properties。
那么当程序运行过程中,比如一个controller中既需要查询数据库A,又需要查询数据库B,而且两者都希望用entity(Mybatis中用于与表结构保持一直的bean)来接收查询结果,即都希望走Mybatis的entity-mapper-mapper.xml这么一套框架。这个时候最原始的方法是在代码中手动链接数据库比如:

 var conn:Connection = null
try {
Class.forName("com.mysql.jdbc.Driver")
conn = DriverManager.getConnection("url","username","password")
val statement = conn.createStatement()
val result = statement.executeQuery("select * from **** where **** ")
while(result.next()){
}
}
本文所采用的是修改dao层context配置文件添加基于Spring事务和AOP方式的注解式数据源切换。最终实现的效果如下:
@Transactional //该注解表明该Service类开启Spring事务,事务的意思是指具有原子性的一个操作集合(本人理解),该事务做什么事在dao层的配置文件里配置,后面会讲。
@Service //表明为Service类,使用Component也行,Spring在启动时会扫描该类将该类所需要的bean全部构建出来以供使用
@TargetDataSource(name = "dataSource1") //重点,自定义的AOP注解,指定该TestService1类下的所有public方法都使用数据源dataSource1
class TestService1{
public void queryAllUser(){
UserMapper userMapper = new UserMapper()
userMapper.queryAllUser();
System.out.println("使用数据源dataSource1查询用户信息")
}
}
@Transactional
@Service
@TargetDataSource(name = "dataSource2")
class TestService2{
public void queryAllBook(){
BookMapper bookMapper = new BookMapper()
bookMapper.queryAllBook();
System.out.println("使用数据源dataSource2查询书籍信息")
}
}
在每一个需要切换数据源的Service层使用TargetDataSource(name= “***”)即可指定当前线程的数据源,当然别忘记@Transactional事务的添加,该事务用于Mybatis查询数据时去获取当前线程的数据源为哪一个。如此在controller中正常调用Service中的方法就行了,如果需要查询两个数据库那么分别调用两个TestService中的方法即可。比如:
//本人目前使用scala语言作为开发语言,Java没怎么写了,还是习惯Scala,以下程序还是使用Scala语言规范哈
class testController{
@AutoWired
TestService1 testService1;
@AutoWired
TestService2 testService2;
@RequestMapping(value = Array("/test"), produces = Array("application/json;charset=UTF-8"), method = Array(RequestMethod.GET))
def test(): Unit = {
val allUser = testService1.queryAllUser()
println("使用TestService1查询数据源1中的所有用户")
val allBook = testService2.queryAllBook("33287")
println("使用TestService2查询数据源2中的所有书籍信息")
}
}

二、如何实现
接下来就详细讲述如何在Spring MVC和Mybatis的单套数据源支持上扩展多数据源切换能力。以下为双数据源,三数据源的实现方式相同。

 1.首先在配置文件中添加第二个数据源的链接信息。
environment-dev.properties
#数据源1的链接信息
db1.jdbc.username=xxx
db1.jdbc.password=xxxxx
db1.jdbc.driverClassName=com.mysql.jdbc.Driver
db1.jdbc.url=xxxx?useUnicode=true&characterEncoding=utf8
#新添加的数据源2的链接信息
db2.jdbc.username=xxx
db2.jdbc.password=xxxxx
db2.jdbc.driverClassName=com.mysql.jdbc.Driver
db2.jdbc.url=xxxx?useUnicode=true&characterEncoding=utf8
2.在dao层的context.xml配置文件中添加基于注解的事务管理以及AOP切面配置
(1)在配置文件中添加双数据源,如下:
<bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db1.jdbc.driverClassName}"/>
<property name="password" value="${db1.jdbc.password}"/>
<property name="username" value="${db1.jdbc.username}"/>
<property name="url" value="${db1.jdbc.url}"/>
<property name="initialSize" value="5"/>
<property name="maxActive" value="10"/>
</bean>
<bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db2.jdbc.driverClassName}"/>
<property name="password" value="${db2.jdbc.password}"/>
<property name="username" value="${db2.jdbc.username}"/>
<property name="url" value="${db2.jdbc.url}"/>
<property name="initialSize" value="5"/>
<property name="maxActive" value="10"/>
</bean>
(2)使用AbstractRoutingDataSource实现动态数据源选择
配置文件中添加
<bean id="dataSource" class="common.dao.mysql.dataSourceManage.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="dataSource1" value-ref="dataSource1" />
<entry key="dataSource2" value-ref="dataSource2" />
</map>
</property>
<!-- 默认使用dataSource1的数据源 -->
<property name="defaultTargetDataSource" ref="dataSource1" />
</bean>
在dao层创建dataSourceManage包,在包中创建如下类DynamicDataSource,DataSourceHolder。
类一:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.getDataSoure();
}
}
类二:
public class DataSourceHolder {
//线程本地环境
private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();
//设置数据源
public static void setDataSource(String customerType) {
dataSources.set(customerType);
}
//获取数据源
public static String getDataSoure() {
return (String) dataSources.get();
}
//清除数据源
public static void clearDataSource() {
dataSources.remove();
}
}
Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。以上完成数据库操作之前的数据源选择,使用的是DataSourceHolder.getDataSoure();

(3)添加Spring事务,确定在业务代码中查询数据库时,由Spring事务去执行以上对数据源的选择,这样既不影响业务代码又能提供事务的性质保证。
在配置文件中添加

 <!-- 定义事务管理器(声明式的事务) -->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 将所有具有@Transactional注解的Bean自动配置为声明式事务支持 -->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager" />
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations">
<list>
<value>classpath:common/dao/mysql/mapper/*Mapper.xml</value>
</list>
</property>
</bean>
注意配置sqlSessionFactory中使用的数据源需要和事务配置中的保持一直。以及配置文件的顶层bean需要添加 xmlns:tx="http://www.springframework.org/schema/tx"和xsi:schemaLocation中添加http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
(4)配置AOP提供Service层注解式声明使用的数据源
首先在配置文件中添加AOP支持xmlns:aop="http://www.springframework.org/schema/aop",xsi:schemaLocation中添加http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
<!--配置切面的bean DataSourceExchange 自定义的切面类实现数据源切换-->
<bean id="dataSourceExchange" class="common.dao.mysql.datasource.DataSourceExchange" />
<!--配置AOP -->
<aop:config>
<!--配置切点表达式 定义dataSourceExchange中的拦截使用范围-->
<aop:pointcut id="servicePointcut" expression="execution(* common.dao.mysql.service.*.*(..))"/>
<aop:advisor advice-ref="dataSourceExchange" pointcut-ref="servicePointcut" order="1" />
</aop:config>
其中execution(* common.dao.mysql.service.*.*(..))为service下的所有类(指TestService1和TestService2)的所有public方法都加上切面代理即使用dataSourceExchange处理。
然后在dataSourceManage包下创建DataSourceExchange类实现AfterReturningAdvice,MethodBeforeAdvice两个aop通知
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
public class DataSourceExchange implements MethodBeforeAdvice, AfterReturningAdvice {
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
DataSourceHolder.clearDataSource();
}
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
//这里TargetDataSource是自定义注解,method为查询数据库的方法比如一中的queryAllUser(),Objects为传给该方法的参数数组,o为调用该方法的对象,比如val allUser =
//testService1.queryAllUser()中的testService1
if (method.isAnnotationPresent(TargetDataSource.class)) {
TargetDataSource dataSource = method.getAnnotation(TargetDataSource.class);
DataSourceHolder.setDataSource(dataSource.name());
} else {
if (o.getClass().isAnnotationPresent(TargetDataSource.class)) {
TargetDataSource dataSource = o.getClass().getAnnotation(TargetDataSource.class);
DataSourceHolder.setDataSource(dataSource.name());
}
}
}
}
然后在dataSourceManage包下创建TargetDataSource注解类
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String name() default "dataSource1";
}
以上配置完成之后即可达成一中的最终效果。
完整的dao配置文件内容如下
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd ">
<context:annotation-config/>
<context:component-scan base-package="com.test.common.dao"/>
<bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db1.jdbc.driverClassName}"/>
<property name="password" value="${db1.jdbc.password}"/>
<property name="username" value="${db1.jdbc.username}"/>
<property name="url" value="${db1.jdbc.url}"/>
<property name="initialSize" value="5"/>
<property name="maxActive" value="10"/>
</bean>
<bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db2.jdbc.driverClassName}"/>
<property name="password" value="${db2.jdbc.password}"/>
<property name="username" value="${db2.jdbc.username}"/>
<property name="url" value="${db2.jdbc.url}"/>
<property name="initialSize" value="5"/>
<property name="maxActive" value="10"/>
</bean>
<bean id="dataSource" class="test.common.dao.mysql.dataSourceManage.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="dataSource1" value-ref="dataSource1" />
<entry key="dataSource2" value-ref="dataSource2" />
</map>
</property>
<!-- 默认使用dataSource1的数据源 -->
<property name="defaultTargetDataSource" ref="dataSource1" />
</bean>
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="dataSourceTransactionManager" />
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations">
<list>
<value>classpath:test/common/dao/mysql/mapper/*Mapper.xml</value>
</list>
</property>
</bean>
<!--配置可以批量执行的sqlSession -->
<!--配置切面的bean -->
<bean id="dataSourceExchange" class="test.common.dao.mysql.datasource.DataSourceExchange" />
<!--配置AOP -->
<aop:config>
<!--配置切点表达式 -->
<aop:pointcut id="servicePointcut" expression="execution(* test.common.dao.mysql.service.*.*(..))"/>
<aop:advisor advice-ref="dataSourceExchange" pointcut-ref="servicePointcut" order="1" />
</aop:config>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="test.common.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
</beans>
最后欢迎大家的评论和指正,随时留言,今后希望能给大家带来更好的技术贴
版权声明
本文为[mapleJia]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/mapleJia/p/13943656.html

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云