Spring AOP實現注解式的Mybatis多數據源切換


一、為什么要使用多數據源切換?
多數據源切換是為了滿足什么業務場景?正常情況下,一個微服務或者說一個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>



  最后歡迎大家的評論和指正,隨時留言,今后希望能給大家帶來更好的技術貼


免責聲明!

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



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