spring boot+druid+mybatisPlus 動態切換數據源

前言

項目中經常會有集成其他數據庫的情況,我們項目是使用spring Boot+Druid+Mybatis Plus開發,本文簡述在項目通過AOP的方式動態的切換數據庫。

版本號

框架 版本號
druid 1.1.10
spring boot 2.2.2.RELEASE
mybatis plus 3.2.0

實現思路

  1. 配置文件中配置多個數據源
  2. 將多個數據源注入到AbstractRoutingDataSource類的一個Map結構中,
  3. 通過AOP的切面來動態的從Map中獲取數據源

實現過程

1. application.yml中配置多數據源

spring:
  datasource:
    druid:
      xuyang:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/clouddb_xy?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=GMT%2B8
        username: hebao-dev
        password: <your password>
        initialSize: 5
        minIdle: 5
        maxActive: 20
      ppe:
        driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
        url: jdbc:sqlserver://localhost:1433; DatabaseName=PowerPPE
        username: sa
        password: <your password>
        initialSize: 5
        minIdle: 5
        maxActive: 20

2. 配置mybatis plus 這一步主要是配置多個數據源和mybatis的sqlSessionFactory

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.powerpms.risun.datasource.DBTypeEnum;
import com.powerpms.risun.datasource.DynamicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
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 org.springframework.transaction.annotation.EnableTransactionManagement;

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

/**
 *  配置多數據源
 * @Author: 俞兆鵬
 * @Date: 2020/2/26 15:52
 * @Email: [email protected]
 * @Version 1.0
 */
@EnableTransactionManagement
@Configuration
@MapperScan("com.powerpms.dao.**")
public class MybatisPlusConfig {
    
    @Bean(name = "xuyang-db")
    @ConfigurationProperties(prefix = "spring.datasource.druid.xuyang")
    public DataSource db1() {
        return DruidDataSourceBuilder.create().build();
    }
    
    @Bean(name = "ppe-db")
    @ConfigurationProperties(prefix = "spring.datasource.druid.ppe")
    public DataSource db2() {
        return DruidDataSourceBuilder.create().build();
    }
    
    @Bean
    @Primary
    public DataSource multipleDataSource(@Qualifier("xuyang-db") DataSource xuyangDb,
                                         @Qualifier("ppe-db") DataSource ppeDb) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DBTypeEnum.xuyang.getValue(), xuyangDb);
        targetDataSources.put(DBTypeEnum.ppe.getValue(), ppeDb);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(xuyangDb);
        return dynamicDataSource;
    }
    
    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(multipleDataSource(db1(), db2()));
        
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        sqlSessionFactory.setConfiguration(configuration);
        return sqlSessionFactory.getObject();
    }

}

3. 實現AbstractRoutingDataSource類,其實切換數據源的核心邏輯都在這個類裏,把這個類看一下就大致明白原理了,後面會專門的說一下這個類

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

/**
 * @Author: 俞兆鵬
 * @Date: 2020/2/26 16:14
 * @Email: [email protected]
 * @Version 1.0
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        // 返回要使用的數據源的key
        return  DbContextHolder.getDbType();
    }
}

4. 實現一個線程安全的容器,用來保存當前線程要使用的數據源的key,這個容器裏的key的設置,就是通過AOP切面來設置的,以mapper爲切點,在執行mapper的時候動態設置當前mapper要使用的數據源

/**
 * @Author: 俞兆鵬
 * @Date: 2020/2/26 16:15
 * @Email: [email protected]
 * @Version 1.0
 */
public class DbContextHolder {
    
    private static final ThreadLocal contextHolder =  new ThreadLocal<>();
    
    /**
     * 設置數據源
     * @param dbTypeEnum
     */
    public static void setDbType(DBTypeEnum dbTypeEnum) {
        contextHolder.set(dbTypeEnum.getValue());
    }
    
    /**
     * 取得當前數據源
     * @return
     */
    public static String getDbType() {
        return (String) contextHolder.get();
    }
    
    /**
     * 清除上下文數據
     */
    public static void clearDbType() {
        contextHolder.remove();
    }
}

5. 實現一個枚舉類用來當作多個數據源的key

/**
 * @Author: 俞兆鵬
 * @Date: 2020/2/26 16:17
 * @Email: [email protected]
 * @Version 1.0
 */
public enum DBTypeEnum {
    xuyang("xuyang-db"),
    ppe("ppe-db");
    
    private String value;
    
    DBTypeEnum(String value) {
        this.value = value;
    }
    
    public String getValue() {
        return value;
    }
}

6. 通過AOP切面動態設置數據源

在使用AOP的時候需要依賴aop的jar包如下

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.5.3</version>
</dependency>

切面類

import com.powerpms.risun.datasource.DBTypeEnum;
import com.powerpms.risun.datasource.DbContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * @Author: 俞兆鵬
 * @Date: 2020/2/26 16:34
 * @Email: [email protected]
 * @Version 1.0
 */
@Component
@Order(value = -100)
@Slf4j
@Aspect
public class DataSourceSwitchAspect {
    
    @Pointcut("execution(* com.powerpms.dao.xy..*.*(..))")
    private void db1Aspect() {
    }
    
    @Pointcut("execution(* com.powerpms.dao.ppe..*.*(..))")
    private void db2Aspect() {
    }

    
    @Before("db1Aspect()")
    public void db1() {
        log.info("切換到旭陽主數據源...");
        DbContextHolder.setDbType(DBTypeEnum.xuyang);
    }
    
    @Before("db2Aspect()")
    public void db2() {
        log.info("切換到PPE數據源...");
        DbContextHolder.setDbType(DBTypeEnum.ppe);
    }
 }

原理

AbstractRoutingDataSource類詳解

  1. 這是一個抽象類,這個類只有一個抽象方法,也是我們實現的這個方法。
	/**
	 * 	 這個方法返回的是當前數據源的key,通過這個key來決定使用哪個數據源
	 * 
	 */
	@Nullable
	protected abstract Object determineCurrentLookupKey();

該方法的調用如下:

	protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
                // 在這裏調用,獲取數據源的key
		Object lookupKey = determineCurrentLookupKey();
                // 根據key來獲取數據源
		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;
	}

我們完成了這一步之後,將這裏獲取到的dataSource對象注入到mybatis的sqlSessionFactory中,如下

    @Bean
    @Primary
    public DataSource multipleDataSource(@Qualifier("xuyang-db") DataSource xuyangDb,
                                         @Qualifier("ppe-db") DataSource ppeDb) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DBTypeEnum.xuyang.getValue(), xuyangDb);
        targetDataSources.put(DBTypeEnum.ppe.getValue(), ppeDb);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(xuyangDb);
        return dynamicDataSource;
    }
    
    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
        // 在這裏注入sqlSessionFactory要使用的數據源,而這裏獲取到的數據源正是我們實現的AbstractRoutingDataSource類所返回的數據源
        sqlSessionFactory.setDataSource(multipleDataSource(db1(), db2()));
        
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        sqlSessionFactory.setConfiguration(configuration);
        return sqlSessionFactory.getObject();
    }

參考

[小塵哥的博客](springboot+druid+mybatis plus的多數據源配置)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章