美文网首页
Spring AOP如何实现注解式的Mybatis多数据源切换详

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

作者: 平凡的柚子 | 来源:发表于2021-01-19 15:02 被阅读0次

    一、为什么要使用多数据源切换?

    多数据源切换是为了满足什么业务场景?正常情况下,一个微服务或者说一个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>
    

    最新2020整理收集的一些高频面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等,需要获取这些内容的朋友请加Q君样:11604713672

    相关文章

      网友评论

          本文标题:Spring AOP如何实现注解式的Mybatis多数据源切换详

          本文链接:https://www.haomeiwen.com/subject/xhddaktx.html