1. 程式人生 > >【十七】Spring Boot 之 多資料來源(擴充套件AbstractRoutingDataSource類,實現動態資料來源。使用AOP註解動態切換資料來源)

【十七】Spring Boot 之 多資料來源(擴充套件AbstractRoutingDataSource類,實現動態資料來源。使用AOP註解動態切換資料來源)

在一個專案中操作多個不同的資料來源時,需要用到多資料來源的配置。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.sid</groupId>
<artifactId>springboot</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>


<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.8.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <!-- spring-boot的web啟動的jar包 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>


    <!-- mysql資料庫連線包-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.38</version>
    </dependency>

    <!-- alibaba的druid資料庫連線池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.9</version>
    </dependency>

</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>

        <!-- mybatis generator 自動生成程式碼外掛 -->
        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.3.2</version>
            <configuration>
                <configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile>
                <overwrite>true</overwrite>
                <verbose>true</verbose>
            </configuration>
        </plugin>

    </plugins>
</build>

  
</project>

application.yml

server:
  port: 8088
  context-path: /sid

spring:
  datasource:
    # 使用druid資料來源
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    druid:
      one:  #資料來源1
        url: jdbc:mysql://localhost:3306/sid
        username: root
        password: root
      two: #資料來源2
          url: jdbc:mysql://localhost:3306/test
          username: root
          password: root
      #初始化時建立物理連線的個數
      initialSize: 1
      #池中最大連線數
      maxActive: 20
      #最小空閒連線
      minIdle: 1
      #獲取連線時最大等待時間,單位毫秒
      maxWait: 60000
      #有兩個含義:
      #1) Destroy執行緒會檢測連線的間隔時間,如果連線空閒時間大於等於minEvictableIdleTimeMillis則關閉物理連線。
      #2) testWhileIdle的判斷依據,詳細看testWhileIdle屬性的說明
      timeBetweenEvictionRunsMillis: 60000
      #連線保持空閒而不被驅逐的最小時間,單位是毫秒
      minEvictableIdleTimeMillis: 300000
      #使用該SQL語句檢查連結是否可用。如果validationQuery=null,testOnBorrow、testOnReturn、testWhileIdle都不會起作用。
      validationQuery: SELECT 1 FROM DUAL
      #建議配置為true,不影響效能,並且保證安全性。申請連線的時候檢測,如果空閒時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連線是否有效。
      testWhileIdle: true
      #申請連線時執行validationQuery檢測連線是否有效,做了這個配置會降低效能。
      testOnBorrow: false
      #歸還連線時執行validationQuery檢測連線是否有效,做了這個配置會降低效能。
      testOnReturn: false
      # 配置監控統計攔截的filters,去掉後監控介面sql無法統計,'wall'用於防火牆
      filters: stat,wall,slf4j
      # 通過connectProperties屬性來開啟mergeSql功能;慢SQL記錄
      #connectionProperties.druid.stat.mergeSql: true
      #connectionProperties.druid.stat.slowSqlMillis: 5000
      # 合併多個DruidDataSource的監控資料
      #useGlobalDataSourceStat: true
      #default-auto-commit: true 預設

## 該配置節點為獨立的節點,不是在在spring的節點下
mybatis:
  mapper-locations: classpath:mapping/*.xml  #注意:一定要對應mapper對映xml檔案的所在路徑
  type-aliases-package: com.sid.model  # 注意:對應實體類的路徑
  configuration:
    #log-impl:  org.apache.ibatis.logging.slf4j.Slf4jImpl
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #控制檯列印sql

定義多個數據源的名字

/**
 * @program: springboot
 * @description:    資料來源名稱
 * @author: Sid
 * @date: 2018-11-22 13:59
 * @since: 1.0
 **/
public interface  DataSourceNames {
    String ONE = "ONE";
    String TWO = "TWO";
}

擴充套件AbstractRoutingDataSource類,實現動態資料來源

package com.sid.configuration.multi.datasource.dynamic;

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

import javax.sql.DataSource;
import java.util.Map;

/**
 * @program: springboot
 * @description: 建立動態資料來源
 * 實現資料來源切換的功能就是自定義一個類擴充套件AbstractRoutingDataSource抽象類,
 * 其實該相當於資料來源DataSourcer的路由中介,
 * 可以實現在專案執行時根據相應key值切換到對應的資料來源DataSource上。
 * @author: Sid
 * @date: 2018-11-22 13:59
 * @since: 1.0
 **/
public class DynamicDataSource extends AbstractRoutingDataSource {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    /**
     * 配置DataSource, defaultTargetDataSource為主資料庫
     */
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        //設定預設資料來源
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        //設定資料來源列表
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    /**
     * 是實現資料來源切換要擴充套件的方法,
     * 該方法的返回值就是專案中所要用的DataSource的key值,
     * 拿到該key後就可以在resolvedDataSource中取出對應的DataSource,
     * 如果key找不到對應的DataSource就使用預設的資料來源。
     * */
    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    /**
     * 綁定當前執行緒資料來源路由的key
     * 使用完成後必須呼叫removeRouteKey()方法刪除
     */
    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    /**
     * 獲取當前執行緒的資料來源路由的key
     */
    public static String getDataSource() {
        return contextHolder.get();
    }

    /**
     * 刪除與當前執行緒繫結的資料來源路由的key
     */
    public static void clearDataSource() {
        contextHolder.remove();
    }

}

配置多個數據源和動態資料來源

package com.sid.configuration.multi.datasource.dynamic;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @program: springboot
 * @description: 動態資料來源配置
 * @author: Sid
 * @date: 2018-11-22 14:01
 * @since: 1.0
 **/
@Configuration
public class DynamicDataSourceConfig {
    /**
     * 建立 ChangeDataSource Bean
     * */
    @Bean
    @ConfigurationProperties("spring.datasource.druid.one")
    public DataSource oneDataSource(){
        DataSource dataSource = DruidDataSourceBuilder.create().build();
        return dataSource;
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.two")
    public DataSource twoDataSource(){
        DataSource dataSource = DruidDataSourceBuilder.create().build();
        return dataSource;
    }

    /**
     * 如果還有資料來源,在這繼續新增 ChangeDataSource Bean
     * */

    @Bean
    @Primary
    public DynamicDataSource dataSource(DataSource oneDataSource, DataSource twoDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>(2);
        targetDataSources.put(DataSourceNames.ONE, oneDataSource);
        targetDataSources.put(DataSourceNames.TWO, twoDataSource);
        // 還有資料來源,在targetDataSources中繼續新增
        System.out.println("DataSources:" + targetDataSources);
        //預設的資料來源是oneDataSource
        return new DynamicDataSource(oneDataSource, targetDataSources);
    }
}

到此動態資料來源配置完成,不需要給不同的資料來源分別寫DAO層,使用的時候切換資料來源的方式:

切換到TOW這個資料來源
DynamicDataSource.setDataSource(DataSourceNames.TWO);

//todo 增刪改查

userMapper.insert(user);

使用完後要清空,這樣就會回到預設資料來源
DynamicDataSource.clearDataSource();

 由於每次做資料庫增刪該查的時候我們都要把邏輯巢狀在切換資料來源和恢復預設資料來源的程式碼之間,這裡可以做一個AOP,使用註解的方式切換資料來源,更方便使用

宣告註解

package com.sid.configuration.multi.datasource.dynamic;

import java.lang.annotation.*;

/**
 * @program: springboot
 * @description: 動態切換資料來源註解
 * @author: Sid
 * @date: 2018-11-22 14:02
 * @since: 1.0
 **/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ChangeDataSource {
    String value() default DataSourceNames.ONE;
}

動態切換資料來源AOP切面處理

package com.sid.configuration.multi.datasource.dynamic;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @program: springboot
 * @description: 動態切換資料來源AOP切面處理
 * @author: Sid
 * @date: 2018-11-22 14:03
 * @since: 1.0
 **/
@Aspect
@Component
public class DataSourceAspect implements Ordered {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 切點: 所有配置 ChangeDataSource 註解的方法
     */
    @Pointcut("@annotation(com.sid.configuration.multi.datasource.dynamic.ChangeDataSource)")
    public void dataSourcePointCut() {}

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        ChangeDataSource ds = method.getAnnotation(ChangeDataSource.class);
        // 通過判斷 @ChangeDataSource註解 中的值來判斷當前方法應用哪個資料來源
        DynamicDataSource.setDataSource(ds.value());
        System.out.println("當前資料來源: " + ds.value());
        logger.debug("set datasource is " + ds.value());
        try {
            return point.proceed();
        } finally {
            DynamicDataSource.clearDataSource();
            logger.debug("clean datasource");
        }
    }
    @Override
    public int getOrder() {
        return 1;
    }
}

使用示例

其中addUserA沒有@ChangeDataSource註解則代表使用預設的DataSource

package com.sid.service.impl;

import com.sid.configuration.multi.datasource.dynamic.ChangeDataSource;
import com.sid.configuration.multi.datasource.dynamic.DataSourceNames;
import com.sid.mapper.UserMapper;
import com.sid.model.User;
import com.sid.service.MultiDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @program: springboot
 * @description:
 * @author: Sid
 * @date: 2018-11-22 14:20
 * @since: 1.0
 **/
@Service
public class MultiDataSourceImpl implements MultiDataSource {
    @Autowired
    private UserMapper userMapper;

    @Override
    public User addUserA(User user) {
        userMapper.insert(user);
        return user;
    }

    @ChangeDataSource(DataSourceNames.TWO)
    @Override
    public User addUserB(User user) {
        userMapper.insert(user);
        return user;
    }
}