Spring+Mybatisダイナミックにデータソースを切り替える方法


機能の需要は会社が大きな運営プラットフォームを作ることです。
1、運営プラットフォームには独自のデータベースがあり、ユーザー、役割、メニュー、部分及び権限などの基本機能を維持する。
2、運営プラットフォームは他の異なるサービス(サービスA、サービスB)のバックグラウンド運営を提供する必要があり、サービスA、サービスBのデータベースは独立している。
したがって、運営プラットフォームは少なくとも3つのライブラリに接続します。運営ライブラリ、Aライブラリ、Bライブラリ、そして各機能要求に対して自動的に対応するデータソースに切り替えることができるようにしたいです。我々のシステムの機能は相互に独立している)。
第一歩:マルチデータソースの設定
1、データソースを定義する:
私が採用しているデータのソースはアリのドルチェ・ソースです。設定は以下の通りです

<!-- op dataSource -->
  <bean id="opDataSource" class="com.alibaba.druid.pool.DruidDataSource"
    init-method="init" destroy-method="close">
    <property name="url" value="${db.master.url}" />
    <property name="username" value="${db.master.user}" />
    <property name="password" value="${db.master.password}" />
    <property name="driverClassName" value="${db.master.driver}" />
    <property name="initialSize" value="5" />
    <property name="maxActive" value="100" />
    <property name="minIdle" value="10" />
    <property name="maxWait" value="60000" />
    <property name="validationQuery" value="SELECT 'x'" />
    <property name="testOnBorrow" value="false" />
    <property name="testOnReturn" value="false" />
    <property name="testWhileIdle" value="true" />
    <property name="timeBetweenEvictionRunsMillis" value="600000" />
    <property name="minEvictableIdleTimeMillis" value="300000" />
    <property name="removeAbandoned" value="true" />
    <property name="removeAbandonedTimeout" value="1800" />
    <property name="logAbandoned" value="true" />
    <!--          filters -->
    <property name="filters" value="config,mergeStat,wall,log4j2" />
    <property name="connectionProperties" value="config.decrypt=true" />
  </bean>

  <!-- serverA dataSource -->
  <bean id="serverADataSource" class="com.alibaba.druid.pool.DruidDataSource"
    init-method="init" destroy-method="close">
    <property name="url" value="${db.serverA.master.url}" />
    <property name="username" value="${db.serverA.master.user}" />
    <property name="password" value="${db.serverA.master.password}" />
    <property name="driverClassName" value="${db.serverA.master.driver}" />
    <property name="initialSize" value="5" />
    <property name="maxActive" value="100" />
    <property name="minIdle" value="10" />
    <property name="maxWait" value="60000" />
    <property name="validationQuery" value="SELECT 'x'" />
    <property name="testOnBorrow" value="false" />
    <property name="testOnReturn" value="false" />
    <property name="testWhileIdle" value="true" />
    <property name="timeBetweenEvictionRunsMillis" value="600000" />
    <property name="minEvictableIdleTimeMillis" value="300000" />
    <property name="removeAbandoned" value="true" />
    <property name="removeAbandonedTimeout" value="1800" />
    <property name="logAbandoned" value="true" />
    <!--          filters -->
    <property name="filters" value="config,mergeStat,wall,log4j2" />
    <property name="connectionProperties" value="config.decrypt=true" />
  </bean>

  <!-- serverB dataSource -->
  <bean id="serverBDataSource" class="com.alibaba.druid.pool.DruidDataSource"
    init-method="init" destroy-method="close">
    <property name="url" value="${db.serverB.master.url}" />
    <property name="username" value="${db.serverB.master.user}" />
    <property name="password" value="${db.serverB.master.password}" />
    <property name="driverClassName" value="${db.serverB.master.driver}" />
    <property name="initialSize" value="5" />
    <property name="maxActive" value="100" />
    <property name="minIdle" value="10" />
    <property name="maxWait" value="60000" />
    <property name="validationQuery" value="SELECT 'x'" />
    <property name="testOnBorrow" value="false" />
    <property name="testOnReturn" value="false" />
    <property name="testWhileIdle" value="true" />
    <property name="timeBetweenEvictionRunsMillis" value="600000" />
    <property name="minEvictableIdleTimeMillis" value="300000" />
    <property name="removeAbandoned" value="true" />
    <property name="removeAbandonedTimeout" value="1800" />
    <property name="logAbandoned" value="true" />
    <!--          filters -->
    <property name="filters" value="config,mergeStat,wall,log4j2" />
    <property name="connectionProperties" value="config.decrypt=true" />
  </bean>
私は三つのデータソースを配置しました。oPDatorSource(運営プラットフォーム自体のデータソース)、serverADaSource、serverBRADWA Source。
2、マルチプレックスDataSourceを配置する
multiple DataSourceは以上の3つのデータソースの1つのプロキシに相当しています。本当にSpring/Mybatisと結合する時multiple DataSourceと単独で構成されたDataSourceは使用していません。

<!-- Spring  Mybatis:  multipleDatasource -->
  <bean id="sqlSessionFactory"
    class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
    <property name="dataSource" ref="multipleDataSource" />
    <!--     Mapping.xml   -->
    <property name="mapperLocations">
      <list>
        <value>classpath*:/sqlMapperXml/*.xml</value>
        <value>classpath*:/sqlMapperXml/*/*.xml</value>
      </list>
    </property>
    <property name="configLocation" value="classpath:xml/mybatis-config.xml"></property>
    <property name="typeAliasesPackage" value="com.XXX.platform.model" />
    <property name="globalConfig" ref="globalConfig" />
    <property name="plugins">
      <array>
        <!--        -->
        <bean id="paginationInterceptor"
          class="com.baomidou.mybatisplus.plugins.PaginationInterceptor">
          <property name="dialectType" value="mysql" />
          <property name="optimizeType" value="aliDruid" />
        </bean>
      </array>
    </property>
  </bean>
  
  <!-- MyBatis      -->
  <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!--  Dao       ,         -->
    <property name="basePackage" value="com.XXX.platform.mapper" />
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
  </bean>  
  <!-- MP      -->
  <bean id="globalConfig" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
    <property name="idType" value="0" />
    <property name="dbColumnUnderline" value="true" />
  </bean>


  <!--       multipleDataSource -->
  <bean id="transactionManager"
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="multipleDataSource"></property>
  </bean>
multileDataSourceの位置を把握した後、次にmultileDataSourceがどのように実現されるかを重点的に見てみます。配置ファイルは以下の通りです。

<bean id="multipleDataSource" class="com.xxxx.platform.commons.db.MultipleDataSource">
    <property name="defaultTargetDataSource" ref="opDataSource" />
    <property name="targetDataSources">
      <map>
        <entry key="opDataSource" value-ref="opDataSource" />
        <entry key="serverADataSource" value-ref="serverADataSource" />
        <entry key="serverBDataSource" value-ref="serverBDataSource" />
      </map>
    </property>
  </bean>
実現されたJavaコードは以下の通りです。説明が必要ではないので、一目瞭然です。

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 
 * @ClassName: MultipleDataSource
 * @Description:        <br>
 * @author: yuzhu.peng
 * @date: 2018 1 12    4:37:25
 */
public class MultipleDataSource extends AbstractRoutingDataSource {
  
  private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<String>();
  
  public static void setDataSourceKey(String dataSource) {
    dataSourceKey.set(dataSource);
  }
  
  @Override
  protected Object determineCurrentLookupKey() {
    return dataSourceKey.get();
  }
  
  public static void removeDataSourceKey() {
    dataSourceKey.remove();
  }
}

springからAbstractRoutingDatasourceを継承し、抽象的な方法determine Curent LookupKeyを実現します。この方法は毎回データベース接続Connectionを獲得する時までに、今回の接続のデータソースDatasourceを決定します。Springのコードを見ることができます。

/*    */
  public Connection getConnection()
    throws SQLException {
    return determineTargetDataSource().getConnection();
  }

  protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    /*   determineCurrentLookupKey     ,          */
    Object lookupKey = determineCurrentLookupKey();
    DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
    if ((dataSource == null) && (((this.lenientFallback) || (lookupKey == null)))) {
      dataSource = this.resolvedDefaultDataSource;
    }
    if (dataSource == null) {
      throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    }
    return dataSource;
  }
  /*    :     multipleDataSource     */
  protected abstract Object determineCurrentLookupKey();

第二ステップ:各要求(Serviceメソッドレベル)は、データソースを動的に切り替える
 発想はSpringのAOP思想を利用して、毎回のService方法を遮断して呼び出し、そして方法の全体パス名によって、multileDataSourceのデータを動的に切り替えるkeyである。私たちのプロジェクトは、異なるサービスすなわち異なるデータベースの操作に対して、互いに独立しているので、同じサービスで異なるデータソースを呼び出すことはあまり推奨されていません。このようにすると、ダイナミックに切り替えが必要かどうかを判断する頻度をDAOレベル、つまりSQLレベルに置く必要があります。また、事務管理もできません。
ダイナミックスイッチングデータソースのAOP実装を見てきました。

import java.lang.reflect.Proxy;

import org.apache.commons.lang.ClassUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;

/**
 *      AOP
 * 
 * @author yuzhu.peng
 * @since 2018-01-15
 */
@Aspect
@Order(1)
public class MultipleDataSourceInterceptor {
  /**
   *                             ,         ,Mapper       *ServiceImpl,              ,         
   * 
   * @param joinPoint
   * @throws Throwable
   */
  @Before("execution(* com.xxxx.platform.service..*.*ServiceImpl.*(..))")
  public void setDataSoruce(JoinPoint joinPoint)
    throws Throwable {
    Class<?> clazz = joinPoint.getTarget().getClass();
    String className = clazz.getName();
    if (ClassUtils.isAssignable(clazz, Proxy.class)) {
      className = joinPoint.getSignature().getDeclaringTypeName();
    }
    //      serverA    serverA   ,           
    if (className.contains(".serverA.")) {
      MultipleDataSource.setDataSourceKey(DBConstant.DATA_SOURCE_serverA);
    }
    else if (className.contains(".serverB.")) {
      MultipleDataSource.setDataSourceKey(DBConstant.DATA_SOURCE_serverB);
    }
    else {
      MultipleDataSource.setDataSourceKey(DBConstant.DATA_SOURCE_OP);
    }
  }
  
  /**
   *       ,              ,             ,          ,            ,     
   * 
   * @param joinPoint
   * @throws Throwable
   */
  @After("execution(* com.xxxx.service..*.*ServiceImpl.*(..))")
  public void removeDataSoruce(JoinPoint joinPoint)
    throws Throwable {
    MultipleDataSource.removeDataSourceKey();
  }
}

すべてのServiceImplメソッドをブロックし、そのデータソースの機能をメソッドの全制限によって判断し、該当するデータソースを選択し、実行後に現在のデータソースをリリースします。注意私はSpringの@Orderを使っています。注釈は次のように述べます。複数のAOPを定義する時、orderはとても役に立ちます。
その他:
最初のプロジェクトには事務が導入されていませんので、すべてOKです。毎回正しいデータソースにアクセスできます。SPringの事務管理に加入すると、データソースを動的に切り替えることができなくなりました。


 orderが小さいほど、先に実行されます。これにより、データソースを動的に切り換えることができ、また、トランザクション(同一のデータソース)を成功的に使用することができる。
以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。