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
# 如果使用了pageHelper分页,数据库方言设置问自动识别,以防止不同数据库的分页方式不同
pagehelper:
  reasonable: true
  support-methods-arguments: true
  params: count=countSql
  row-bounds-with-count: true
  auto-dialect: true

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();
    }

注意

当我使用了这种动态切换数据源的方式之后,就不能在使用默认的事务管理器了,默认的事务管理器还是使用的我们标记为primary的数据源。
因此需要我们自己实现事务管理器
自己实现数据源的博客后面会写到

参考

[小尘哥的博客](springboot+druid+mybatis plus的多数据源配置)

猜你喜欢

转载自blog.csdn.net/zhaopeng_yu/article/details/104523289