springboot集成多数据源

也许不会看见 2020-01-29

简要原理:

1)DataSourceEnum列出所有的数据源的key---key

2)DataSourceHolder是一个线程安全的DataSourceEnum容器,并提供了向其中设置和获取DataSourceEnum的方法

3)DynamicDataSource继承AbstractRoutingDataSource并重写其中的方法determineCurrentLookupKey(),在该方法中使用DataSourceHolder获取当前线程的DataSourceEnum

4)MyBatisConfig中生成2个数据源DataSource的bean---value

5)MyBatisConfig中将1)和4)组成的key-value对写入到DynamicDataSource动态数据源的targetDataSources属性(当然,同时也会设置2个数据源其中的一个为DynamicDataSource的defaultTargetDataSource属性中)

6)将DynamicDataSource作为primary数据源注入到SqlSessionFactory的dataSource属性中去,并且该dataSource作为transactionManager的入参来构造DataSourceTransactionManager

7)使用spring aop根据不同的包设置不同的数据源(DataSourceExchange),先使用DataSourceHolder设置将要使用的数据源key

注意:在mapper层进行操作的时候,会先调用determineCurrentLookupKey()方法获取一个数据源(获取数据源:先根据设置去targetDataSources中去找,若没有,则选择defaultTargetDataSource),之后在进行数据库操作。

DataSourceEnum

package com.theeternity.common.dataSource;

/**
 * @program: boxApi
 * @description: 多数据源枚举类
 * @author: tonyzhang
 * @create: 2018-12-18 11:14
 */
public enum DataSourceEnum {
    /**
     * @Description: DS1数据源1, DS2数据源2
     * @Param:
     * @return:
     * @Author: tonyzhang
     * @Date: 2018-12-18 11:20
     */
    DS1("ds1"), DS2("ds2");

    private String key;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    DataSourceEnum(String key) {
        this.key = key;
    }
}

DataSourceHolder

作用:构建一个DatabaseType容器,并提供了向其中设置和获取DataSourceEnmu的方法

package com.theeternity.common.dataSource;

/**
 * @program: boxApi
 * @description: DynamicDataSourceHolder用于持有当前线程中使用的数据源标识
 * @author: tonyzhang
 * @create: 2018-12-18 11:16
 */
public class DataSourceHolder {
    private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();

    public static void setDataSources(String dataSource) {
        dataSources.set(dataSource);
    }

    public static String getDataSources() {
        return dataSources.get();
    }
}

DynamicDataSource

作用:使用DatabaseContextHolder获取当前线程的DataSourceEnmu

package com.theeternity.common.dataSource;

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

/**
 * @program: boxApi
 * @description: DynamicDataSource的类,继承AbstractRoutingDataSource并重写determineCurrentLookupKey方法
 * @author: tonyzhang
 * @create: 2018-12-18 11:17
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceHolder.getDataSources();
    }
}

MyBatisConfig

作用:
通过读取application-test.yml文件生成两个数据源(writeDS、readDS)
使用以上生成的两个数据源构造动态数据源dataSource
@Primary:指定在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@Autowire注解报错(一般用于多数据源的情况下)
@Qualifier:指定名称的注入,当一个接口有多个实现类的时候使用(在本例中,有两个DataSource类型的实例,需要指定名称注入)
@Bean:生成的bean实例的名称是方法名(例如上边的@Qualifier注解中使用的名称是前边两个数据源的方法名,而这两个数据源也是使用@Bean注解进行注入的)
通过动态数据源构造SqlSessionFactory和事务管理器(如果不需要事务,后者可以去掉)

package com.theeternity.beans.mybatisConfig;

import com.theeternity.common.dataSource.DataSourceEnum;
import com.theeternity.common.dataSource.DynamicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

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

/**
 * @program: ApiBoot
 * @description: 动态数据源配置
 * @author: TheEternity Zhang
 * @create: 2019-02-18 11:04
 */
@Configuration
public class MyBatisConfig {

    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> dataSourceType;

    @Value("${mybatis.type-aliases-package}")
    private String basicPackage;

    @Value("${mybatis-plus.mapper-locations}")
    private String mapperLocation;


    @Bean(name="writeDS")
    @ConfigurationProperties(prefix = "primary.datasource.druid")
    public DataSource writeDataSource() {
        return DataSourceBuilder.create().type(dataSourceType).build();
    }
    /**
     * 有多少个从库就要配置多少个
     * @return
     */
    @Bean(name = "readDS")
    @ConfigurationProperties(prefix = "back.datasource.druid")
    public DataSource readDataSourceOne(){
        return DataSourceBuilder.create().type(dataSourceType).build();
    }


    /**
     * @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错
     * @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例)
     */
    @Bean
    @Primary
    public DynamicDataSource dataSource(@Qualifier("writeDS") DataSource writeDS,
                                        @Qualifier("readDS") DataSource readDS) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceEnum.DS1.getKey(), writeDS);
        targetDataSources.put(DataSourceEnum.DS2.getKey(), readDS);

        DynamicDataSource dataSource =new DynamicDataSource();
        // 该方法是AbstractRoutingDataSource的方法
        dataSource.setTargetDataSources(targetDataSources);
        // 默认的datasource设置为writeDS
        dataSource.setDefaultTargetDataSource(writeDS);

        return dataSource;
    }

    /**
     * 根据数据源创建SqlSessionFactory
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(DynamicDataSource dataSource) throws Exception {
        SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
        // 指定数据源(这个必须有,否则报错)
        fb.setDataSource(dataSource);
        // 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加
        // 指定基包
        fb.setTypeAliasesPackage(basicPackage);
        fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocation));
        return fb.getObject();
    }

    /**
     * 配置事务管理器
     */
    @Bean
    public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
        return new DataSourceTransactionManager(dataSource);
    }
}

DataSourceExchange

package com.theeternity.common.aop;

import com.theeternity.common.dataSource.DataSourceEnum;
import com.theeternity.common.dataSource.DataSourceHolder;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * @program: boxApi
 * @description: 数据源自动切换AOP
 * @author: tonyzhang
 * @create: 2018-12-18 11:20
 */
@Aspect
@Component
public class DataSourceExchange {

    private Logger logger= LoggerFactory.getLogger(DataSourceExchange.class);

    @Pointcut("execution(* com.theeternity.core.*.service..*(..))")
    public void pointcut(){}

    /** 
     * @Description: 在service方法开始之前切换数据源
     * @Param: [joinPoint] 
     * @return: void 
     * @Author: tonyzhang 
     * @Date: 2018-12-18 11:28
     */ 
    @Before(value="pointcut()")
    public void before(JoinPoint joinPoint){
        //获取目标对象的类类型
        Class<?> aClass = joinPoint.getTarget().getClass();
        String c = aClass.getName();
        System.out.println("作用包名:"+c);
        String[] ss = c.split("\\.");
        //获取包名用于区分不同数据源
        String packageName = ss[3];
        System.out.println("包名:"+packageName);
        if ("AutoGenerator".equals(packageName)) {
            DataSourceHolder.setDataSources(DataSourceEnum.DS1.getKey());
            logger.info("数据源:"+DataSourceEnum.DS1.getKey());
        } else {
            DataSourceHolder.setDataSources(DataSourceEnum.DS2.getKey());
            logger.info("数据源:"+DataSourceEnum.DS2.getKey());
        }
    }

    /** 
     * @Description: 执行完毕之后将数据源清空 
     * @Param: [joinPoint] 
     * @return: void 
     * @Author: tonyzhang 
     * @Date: 2018-12-18 11:27
     */ 
    @After(value="pointcut()")
    public void after(JoinPoint joinPoint){
        DataSourceHolder.setDataSources(null);

    }
}

屏蔽springboot自带的自动注册数据源

很多朋友反映遇到数据源循环依赖的问题,可以试一下将MyBatisConfig中的相关代码换成这样试试

首先要将spring boot自带的DataSourceAutoConfiguration禁掉,因为它会读取application.properties文件的spring.datasource.*属性并自动配置单数据源。在@SpringBootApplication注解中添加exclude属性即可:

package com.theeternity.core;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@SpringBootApplication(scanBasePackages = "com.theeternity",exclude = {DataSourceAutoConfiguration.class})
/**
 * 全局配置,扫描指定包下的dao接口,不用每个dao接口上都写@Mapper注解了
 */
@MapperScan("com.theeternity.core.*.dao")
public class CoreApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoreApplication.class, args);
    }

}

如果不屏蔽DataSourceAutoConfiguration可以使用如下测试一下(待测试)

将MyBatisConfig中SqlSessionFactory的构建方法改为下面的

@Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("writeDS") DataSource writeDS,
                                               @Qualifier("readDS") DataSource readDS) throws Exception{
        SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
        fb.setDataSource(this.dataSource(writeDS, readDS));
        fb.setTypeAliasesPackage(basicPackage);
          fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocation));
        return fb.getObject();
    }

主配置文件

#配置使用的文件
spring:
  profiles:
    active: test,redis
  devtools:
    restart:
      enabled: true
      additional-paths: src/main/java
#配置tomcat端口及路径
server:
  port: 8088
  servlet:
    context-path: /core
#mybatis配置
mybatis:
  mapper-locations: classpath*:/mybatis-mapper/*.xml
  type-aliases-package: com.theeternity.core.*.entity
  configuration:
    map-underscore-to-camel-case: true #驼峰命名
#mybatis plus配置
mybatis-plus:
  mapper-locations: classpath*:/mybatis-plus-mapper/*.xml
  # MyBaits 别名包扫描路径,通过该属性可以给包中的类注册别名,注册后在 Mapper 对应的 XML 文件中可以直接使用类名,而不用使用全限定的类名
  type-aliases-package: com.theeternity.core.*.entity
  # 数据库表与实体类的驼峰命名自动转换
  configuration:
    map-underscore-to-camel-case: true

配置文件application-test.yml

spring:
  datasource:
    #使用druid连接池
    type: com.alibaba.druid.pool.DruidDataSource

# 自定义的主数据源配置信息
primary:
  datasource:
    #druid相关配置
    druid:
      #监控统计拦截的filters
      filters: stat
      driverClassName: com.mysql.cj.jdbc.Driver
      #配置基本属性
      url: jdbc:mysql://localhost:3306/wechatMVC?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
      username: ***
      password: ***
      #配置初始化大小/最小/最大
      initialSize: 1
      minIdle: 1
      maxActive: 20
      #获取连接等待超时时间
      maxWait: 60000
      #间隔多久进行一次检测,检测需要关闭的空闲连接
      timeBetweenEvictionRunsMillis: 60000
      #一个连接在池中最小生存的时间
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 'x'
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      #打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
      poolPreparedStatements: false
      maxPoolPreparedStatementPerConnectionSize: 20

# 自定义的从数据源配置信息
back:
  datasource:
    #druid相关配置
    druid:
      #监控统计拦截的filters
      filters: stat
      driverClassName: com.mysql.cj.jdbc.Driver
      #配置基本属性
      url: jdbc:mysql://localhost:3306/mycrm?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
      username: ***
      password: ***
      #配置初始化大小/最小/最大
      initialSize: 1
      minIdle: 1
      maxActive: 20
      #获取连接等待超时时间
      maxWait: 60000
      #间隔多久进行一次检测,检测需要关闭的空闲连接
      timeBetweenEvictionRunsMillis: 60000
      #一个连接在池中最小生存的时间
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 'x'
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      #打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
      poolPreparedStatements: false
      maxPoolPreparedStatementPerConnectionSize: 20

参考文档:

https://www.cnblogs.com/java-zhao/p/5413845.html (主参考流程)

http://www.cnblogs.com/java-zhao/p/5415896.html (转aop更改数据源)

https://blog.csdn.net/maoyeqiu/article/details/74011626 (将datasource注入bean简单方法)

https://blog.csdn.net/neosmith/article/details/61202084(将spring boot自带的DataSourceAutoConfiguration禁掉)

相关推荐