SpringBoot+Druid+Mybatis多数据源动态切换

Spring原生支持多数据源动态切换,继承AbstractRoutingDataSource类重写determineCurrentLookupKey方法即可,为了方便切换可通过切面拦截自定义注解实现,代码如下:
1、SpringBoot使用Druid配置多数据源

spring:
  #出现错误时, 直接抛出异常(便于异常统一处理,否则捕获不到404)
  mvc:
    throw-exception-if-no-handler-found: true
    view:
      prefix: classpath:/static/
      suffix: .html
    static-path-pattern: /**

  resources:
    static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/templates/

  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    encoding: utf-8
    cache: false
    mode: LEGACYHTML5
    enabled: true

  http:
    encoding:
      force: true
      charset: UTF-8
      force-request: true
      enabled: true
  datasource:
    #配置DruidDatasouce连接池
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      #连接数据库
      driver-class-name: oracle.jdbc.driver.OracleDriver
      #druid配置详情信息
      max-active: 100  #最大连接数
      initial-size: 1  #初始化连接数
      max-wait: 60000  #获取最大等待时间
      min-idle: 1 #最小连接数
      validation-query: select * from dual
      time-between-eviction-runs-millis: 60000   #一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000     #间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      pool-prepared-statements: false            #是否缓存preparedStatement 在mysql的环境下建议关闭 因为对数据库性能消耗大
      max-open-prepared-statements: 50
      max-pool-prepared-statement-per-connection-size: 20
      filters: stat,wall #配置监控统计拦截的filters,去掉后监控界面SQL无法进行统计,'wall'用于防火墙
      stat-view-servlet:
        url-pattern: /druid/*

      druidDataSource1:
        url: jdbc:oracle:thin:@localhost:1521:orcl
        username: rim
        password: rim

      druidDataSource2:
        url: jdbc:oracle:thin:@localhost:1521:orcl
        username: rim
        password: rim

      druidDataSource3:
        url: jdbc:oracle:thin:@localhost:1521:orcl
        username: rim
        password: rim

2、用于声明数据源的自定义注解TargetDataSource

package com.bsoft.core.datasource;

import com.bsoft.commons.constant.DataSourceKey;

import java.lang.annotation.*;

/**
 * @author :Liujian
 * @date :2019/11/15 16:48
 * @description:自定义DataSource注解
 * @version:
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {

    DataSourceKey value() default DataSourceKey.FIRST;
}

3、定义继承AbstractRoutingDataSource类重写determineCurrentLookupKey方法用于实现数据源动态切换

package com.bsoft.core.datasource;

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

/**
 * @author :Liujian
 * @date :2019/11/15 16:48
 * @description:动态切换数据源
 * @version:
 */
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    /**
     * 获取与数据源相关的key
     * 此key是Map resolvedDataSources 中与数据源绑定的key值
     * 在通过determineTargetDataSource获取目标数据源时使用
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceHolder.getDataSourceRouterKey();
    }
}

4、定义数据源的连接字符串线程绑定DynamicDataSourceHolder

package com.bsoft.core.datasource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * @author :Liujian
 * @date :2019/11/15 16:48
 * @description:动态切换数据源
 * @version:
 */
public class DynamicDataSourceHolder {

    private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceHolder.class);

    /**
     * 存储已经注册的数据源的key
     */
    public static final List<Object> dataSourceKeys = new ArrayList<>();

    /**
     * 线程级别的私有变量
     */
    private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();

    public static String getDataSourceRouterKey () {
        return HOLDER.get();
    }

    public static void setDataSourceRouterKey (String dataSourceRouterKey) {
        logger.info("切换至{}数据源", dataSourceRouterKey);
        HOLDER.set(dataSourceRouterKey);
    }

    /**
     * 设置数据源之前一定要先移除
     */
    public static void removeDataSourceRouterKey () {
        HOLDER.remove();
    }

    /**
     * 判断指定DataSrouce当前是否存在
     *
     * @param dataSourceId
     * @return
     */
    public static boolean containsDataSource(String dataSourceId){
        return dataSourceKeys.contains(dataSourceId);
    }

}

5、SpringBoot的数据源配置类DataSourceConfig

package com.bsoft.core.config;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.bsoft.commons.constant.DataSourceKey;
import com.bsoft.core.datasource.DynamicDataSourceHolder;
import com.bsoft.core.datasource.DynamicRoutingDataSource;
import com.bsoft.core.datasource.DynamicRoutingDataSourceTransactionFactory;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

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

/**
 * @author :Liujian
 * @date :2019/11/15 20:52
 * @description:
 * @version:
 */
@Configuration
public class DataSourceConfig {

    private static Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);

    @Bean(name = "first")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.druid.druidDataSource1")
    public DataSource first() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "second")
    @ConfigurationProperties(prefix = "spring.datasource.druid.druidDataSource2")
    public DataSource second() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "third")
    @ConfigurationProperties(prefix = "spring.datasource.druid.druidDataSource3")
    public DataSource third() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicRoutingDataSource")
    public DataSource dynamicRoutingDataSource(){
        DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();

        Map<Object, Object> dataSourceMap = new HashMap<Object, Object>(3);

        dataSourceMap.put(DataSourceKey.FIRST.getValue(), first());

        dataSourceMap.put(DataSourceKey.SECOND.getValue(), second());

        dataSourceMap.put(DataSourceKey.THIRD.getValue(), third());


        dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
        DynamicDataSourceHolder.dataSourceKeys.addAll(dataSourceMap.keySet());

        return dynamicRoutingDataSource;
    }


    /**
     * 事务
     * @return
     */
    @Bean("transactionManager")
    public PlatformTransactionManager transactionManager(){
        return new DataSourceTransactionManager(dynamicRoutingDataSource());
    }

    @Bean(name="sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactorys() throws Exception {
        logger.info("--------------------  sqlSessionFactory init ---------------------");
        try {
            SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
            sessionFactoryBean.setDataSource(dynamicRoutingDataSource());
            sessionFactoryBean.setTransactionFactory(new DynamicRoutingDataSourceTransactionFactory());
            // 读取配置
            sessionFactoryBean.setTypeAliasesPackage("com.bsoft.*.entity");

            //设置mapper.xml文件所在位置
            Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml");
            sessionFactoryBean.setMapperLocations(resources);
            return sessionFactoryBean.getObject();
        } catch (IOException e) {
            logger.error("mybatis resolver mapper*xml is error", e);
            return null;
        } catch (Exception e) {
            logger.error("mybatis sqlSessionFactoryBean create error", e);
            return null;
        }
    }

}

6、定义拦截数据源注解切换的切面类DynamicDataSourceAspect

package com.bsoft.core.aspect;

import com.bsoft.commons.constant.DataSourceKey;
import com.bsoft.core.datasource.DynamicDataSourceHolder;
import com.bsoft.core.datasource.TargetDataSource;
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.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @author :Liujian
 * @date :2019/11/20 12:10
 * @description:
 * @version:
 */
@Aspect
@Component
@Order(0)
public class DynamicDataSourceAspect {

    private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

    @Pointcut("execution(* com.bsoft..service.impl.*.*(..))")
    public void dataSourcePointcut(){

    }

    @Before("dataSourcePointcut()")
    public void before(JoinPoint joinPoint) {
        Object target = joinPoint.getTarget();
        String method = joinPoint.getSignature().getName();
        Class<?>[] clazz = target.getClass().getInterfaces();
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
        try {
            Method m = clazz[0].getMethod(method, parameterTypes);
            if (clazz[0].isAnnotationPresent(TargetDataSource.class)) {
                TargetDataSource source = clazz[0].getAnnotation(TargetDataSource.class);
                DynamicDataSourceHolder.removeDataSourceRouterKey();
                DynamicDataSourceHolder.setDataSourceRouterKey(source.value().getValue());
            } else if (m != null && m.isAnnotationPresent(TargetDataSource.class)) { //如果方法上存在切换数据源的注解,则根据注解内容进行数据源切换
                TargetDataSource source = clazz[0].getAnnotation(TargetDataSource.class);
                DynamicDataSourceHolder.removeDataSourceRouterKey();
                DynamicDataSourceHolder.setDataSourceRouterKey(source.value().getValue());
                logger.info(String.format("class[%s],method[%s],使用数据源[%s]", clazz[0].getName(), m.getName(),
                        DynamicDataSourceHolder.getDataSourceRouterKey()));
            } else {
                DynamicDataSourceHolder.removeDataSourceRouterKey();
                DynamicDataSourceHolder.setDataSourceRouterKey(DataSourceKey.FIRST.getValue());
                logger.debug("switch datasource fail,use default");
            }
        } catch (Exception e) {
            logger.error("current thread " + Thread.currentThread().getName() + " add data to ThreadLocal error", e);
        }
    }

    @After("dataSourcePointcut()")
    public void after(JoinPoint joinPoint){
        DynamicDataSourceHolder.removeDataSourceRouterKey();
    }
}

7、代码测试
在类或方法上加自定义注解实现拦截并动态切换

@TargetDataSource(DataSourceKey.THIRD)

以上,已完成动态切换,但是开启声明式事务后切换会存在问题,请参考我另一篇博文https://blog.csdn.net/kugeliujian/article/details/103318505

你可能感兴趣的:(java相关)