文章目录

  • 一.前言
  • 二.抽象类-AbstractRoutingDataSource
  • 1.概述
  • 2.源码分析
  • 三.具体实现
  • 1.引入依赖
  • 2.配置文件application.yml
  • 2.1.修改配置文件
  • 2.2.增加测试数据库1
  • 2.3.增加测试数据库2
  • 2.4.增加测试数据库3
  • 3.动态切换数据源的上下文
  • 4.动态数据源
  • 5.动态数据源配置类
  • 3.自定义注解
  • 7.动态数据源切面类(多数据源动态切换)
  • 8.启动类取消自动配置数据源
  • 9.测试
  • 10.动态加载数据源
  • 10.1.新增获取Spring容器工具类
  • 10.2.动态数据源服务类
  • 10.4.测试
  • 11. 关于事务
  • 11.1.单个数据源测试


一.前言

普及知识

  • 一个数据源,也就代表一个数据库,源=数据的源头
  • 数据源实例:一个数据库连接,就代表一个数据源实例对象;
  • 多数据源实例:多个数据库连接对象

什么是动态多数据源

  • 简单来说,可以在应用运行中,将数据源动态生成并可以切换使用。

动态多数据源好处

  • 省去了创建大量的 Bean 的操作;
  • 可以在运行时添加数据源;

实现方式一:

  • 多DataSource + 多SqlSessionFactory 在使用Dao层的时候通过不同的SessionFactory进行处理,一般的情况下我们都是使用Mybatis,配置多个DataSource,每个DataSource扫描不同的Mapper、注入到不同SqlSessionFactory实现的多数据源。
  • 这种方式一旦新增新的数据源、又要增加新的配置代码,重新编译代码。只能实现静态多数据源加载与切换

实现方式二:

要求:
公司云平台项目每个商户一个数据库,所以在写后台代码时,需要根据应用层传递过来的商户id,进行动态切换数据源。

  • 因此方式一就不能满足这个需求,所以就引出了方式二
  • 使用AbstractRoutingDataSource的实现类通过AOP或者手动处理实现动态的使用我们的数据源, 基于这种方式,不仅可是实现真正意义上的多数据源的动态切换,还可以实现在程序的运行过程中,实现动态加载一个或多个新的数据源。

本文主要讲如何通过继承AbstractRoutingDataSource类实现多数据源的动态切换与加载

二.抽象类-AbstractRoutingDataSource

1.概述

SpringBoot提供了AbstractRoutingDataSource类,可以根据用户定义的规则选择当前的数据源,这样我们可以在每次数据库查询操作前执之前,设置使用的数据源。从而实现动态加载、切换数据源。

  • 该类位于 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
  • 它的抽象方法 determineCurrentLookupKey() 方法可以让用户根据自己定义的规则在某一个SQL执行之前动态地选择想要的数据源

2.源码分析

分析一下AbstractRoutingDataSource抽象类的源码

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    private Map<Object, Object> targetDataSources;
    private Object defaultTargetDataSource;
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    private Map<Object, DataSource> resolvedDataSources;
    private DataSource resolvedDefaultDataSource;

    public AbstractRoutingDataSource() {
    }
	
	//关键:以Map的结构存储的我们配置的多个数据源的键值对 key为数据源名称,value为对应数据源
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }
	//关键: 设置默认数据源
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        this.defaultTargetDataSource = defaultTargetDataSource;
    }

    public void setLenientFallback(boolean lenientFallback) {
        this.lenientFallback = lenientFallback;
    }

    public void setDataSourceLookup(DataSourceLookup dataSourceLookup) {
        this.dataSourceLookup = (DataSourceLookup)(dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
    }
	
	//关键:数据源重新赋值
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        } else {
            this.resolvedDataSources = new HashMap(this.targetDataSources.size());
            Iterator var1 = this.targetDataSources.entrySet().iterator();

            while(var1.hasNext()) {
                Entry<Object, Object> entry = (Entry)var1.next();
                Object lookupKey = this.resolveSpecifiedLookupKey(entry.getKey());
                DataSource dataSource = this.resolveSpecifiedDataSource(entry.getValue());
                this.resolvedDataSources.put(lookupKey, dataSource);
            }

            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }

        }
    }

    protected Object resolveSpecifiedLookupKey(Object lookupKey) {
        return lookupKey;
    }

    protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
        if (dataSource instanceof DataSource) {
            return (DataSource)dataSource;
        } else if (dataSource instanceof String) {
            return this.dataSourceLookup.getDataSource((String)dataSource);
        } else {
            throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
        }
    }

    public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }

    public Connection getConnection(String username, String password) throws SQLException {
        return this.determineTargetDataSource().getConnection(username, password);
    }

    public <T> T unwrap(Class<T> iface) throws SQLException {
        return iface.isInstance(this) ? this : this.determineTargetDataSource().unwrap(iface);
    }

    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface);
    }
	//关键:如何选择数据源
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        //关键:就是在这里调用我们实现的determineCurrentLookupKey方法返回的名称
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource 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 + "]");
        } else {
            return dataSource;
        }
    }
	//关键:需要重写的方法,就是根据当前方法的名称查找对应的数据源的,查询不到则使用默认数据源
	// 	用当前返回的Object去作为key去targetDataSources中查找相应的值,如果查找到相对应的DataSource,那么就使用此DataSource获取数据库连接。没有就使用默认数据源。
    protected abstract Object determineCurrentLookupKey();
}

对于该抽象类,了解两组变量即可:

  • Map<Object, Object> targetDataSources和Object defaultTargetDataSource
  • Map<Object, DataSource> resolvedDataSources和DataSource resolvedDefaultDataSource,这两组变量是相互对应的。
  • 当有多个数据源的时候,需要手动调用``setDefaultTargetDataSource方法指定一个作为默认数据源
  • targetDataSources 是暴露给外部用来赋值的,而 resolvedDataSources 是程序内部执行时的依据,因此resolvedDataSources会有一个赋值操作,如下图所示:
  • 每次执行afterPropertiesSet()方法时,都会遍历targetDataSources内的所有元素赋值给resolvedDataSources
  • 因此在外部每新增一个新的数据源,需要手动调用afterPropertiesSet(),从而实现数据源的动态加载
  • 继承该抽象类的时候,必须实现一个抽象方法:protected abstract Object determineCurrentLookupKey(),用于指定到底需要使用哪一个数据源。
  • 多数据源动态切换 java druid多数据源切换_druid

实现动态数据源逻辑

  1. 自定义DynamicDataSource类继承AbstractRoutingDataSource类并实现determineCurrentLookupKey方法(具体逻辑是从当前线程的ThreadLocal中获取我们在某一个SQL执行之前通过AOP切面动态指定的数据源名称);
  2. application.yml中配置多个数据源;
  3. 解析在application.yml中配置的多个数据源,然后生成DynamicDataSource实例,并设置默认数据源(defaultTargetDataSource)和其他数据源(targetDataSources),然后通过afterPropertiesSet()方法将数据源分别进行复制resolvedDataSourcesresolvedDefaultDataSource中。
  4. 定义SqlSessionFactory并注入DynamicDataSource以及MapperLocations
  5. 调用AbstractRoutingDataSource的#getConnection的方法的时候,会先调用determineTargetDataSource方法获取具体的数据源,而在这个方法中会进一步调用我们在DynamicDataSource类中自定义的determineCurrentLookupKey方法,最后在返回DataSource后再进行getConnection的调用。剩下就是具体的SQL逻辑执行了。
  6. 启动类或者配置类使用@MapperScan注解 扫描Mapper接口以及引用sqlSessionFactory

三.具体实现

1.引入依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!--关键: aop框架-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--关键: 持久化框架-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
        
        <!--关键: mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!--关键: druid数据库连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        
        <!--自动生成代码工具-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>

2.配置文件application.yml

2.1.修改配置文件

一个d01一个db02,我这边只配2个 需要多个的 增加即可

spring:
  datasource:
    db01:
      driverClassName: com.mysql.cj.jdbc.Driver
      password: root
      url: jdbc:mysql://localhost:3306/springboot_quartz1?characterEncoding=utf-8&serverTimezone=UTC
      username: root
    db02:
      driverClassName: com.mysql.cj.jdbc.Driver
      password: root
      url: jdbc:mysql://localhost:3306/springboot_quartz2?characterEncoding=utf-8&serverTimezone=UTC
      username: root
2.2.增加测试数据库1
CREATE DATABASE /*!32312 IF NOT EXISTS*/`springboot_quartz1` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */;

USE `springboot_quartz1`;

DROP TABLE IF EXISTS `sys_task`;

CREATE TABLE `sys_task` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `job_name` varchar(255) DEFAULT NULL COMMENT '任务名',
  `description` varchar(255) DEFAULT NULL COMMENT '任务描述',
  `cron_expression` varchar(255) DEFAULT NULL COMMENT 'cron表达式',
  `bean_class` varchar(255) DEFAULT NULL COMMENT '任务执行时调用哪个类的方法 包名+类名',
  `job_status` varchar(255) DEFAULT NULL COMMENT '任务状态',
  `job_group` varchar(255) DEFAULT NULL COMMENT '任务分组',
  `create_user` varchar(64) DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_user` varchar(64) DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=33 DEFAULT CHARSET=utf8;


insert  into `sys_task`(`id`,`job_name`,`description`,`cron_expression`,`bean_class`,`job_status`,`job_group`,`create_user`,`create_time`,`update_user`,`update_time`) values 
(1,'1111','1111','1111','1111','1111','1111','1111','2021-04-15 17:04:55','1111','2021-04-11 17:04:59');
2.3.增加测试数据库2
CREATE DATABASE /*!32312 IF NOT EXISTS*/`springboot_quartz2` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */;

USE `springboot_quartz2`;

DROP TABLE IF EXISTS `sys_task`;

CREATE TABLE `sys_task` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `job_name` varchar(255) DEFAULT NULL COMMENT '任务名',
  `description` varchar(255) DEFAULT NULL COMMENT '任务描述',
  `cron_expression` varchar(255) DEFAULT NULL COMMENT 'cron表达式',
  `bean_class` varchar(255) DEFAULT NULL COMMENT '任务执行时调用哪个类的方法 包名+类名',
  `job_status` varchar(255) DEFAULT NULL COMMENT '任务状态',
  `job_group` varchar(255) DEFAULT NULL COMMENT '任务分组',
  `create_user` varchar(64) DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_user` varchar(64) DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=33 DEFAULT CHARSET=utf8;


insert  into `sys_task`(`id`,`job_name`,`description`,`cron_expression`,`bean_class`,`job_status`,`job_group`,`create_user`,`create_time`,`update_user`,`update_time`) values 
(1,'2222','2222','2222','2222','2222','2222','2222','2021-04-15 17:04:55','2222','2021-04-11 17:04:59');
2.4.增加测试数据库3
CREATE DATABASE /*!32312 IF NOT EXISTS*/`springboot_quartz2` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */;

USE `springboot_quartz3`;

DROP TABLE IF EXISTS `sys_task`;

CREATE TABLE `sys_task` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `job_name` varchar(255) DEFAULT NULL COMMENT '任务名',
  `description` varchar(255) DEFAULT NULL COMMENT '任务描述',
  `cron_expression` varchar(255) DEFAULT NULL COMMENT 'cron表达式',
  `bean_class` varchar(255) DEFAULT NULL COMMENT '任务执行时调用哪个类的方法 包名+类名',
  `job_status` varchar(255) DEFAULT NULL COMMENT '任务状态',
  `job_group` varchar(255) DEFAULT NULL COMMENT '任务分组',
  `create_user` varchar(64) DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_user` varchar(64) DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=33 DEFAULT CHARSET=utf8;


insert  into `sys_task`(`id`,`job_name`,`description`,`cron_expression`,`bean_class`,`job_status`,`job_group`,`create_user`,`create_time`,`update_user`,`update_time`) values 
(1,'3333','3333','3333','3333','3333','3333','3333','2021-04-15 17:04:55','3333','2021-04-11 17:04:59');

3.动态切换数据源的上下文

自定义一个动态数据源上下文类,该类通过ThreadLocal的静态常量存储当前线程是需要访问哪一个数据源。方便在同一个线程上下文中共享数据源名称

public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal();

    public static String get() {
        return contextHolder.get();
    }

    public static void set(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    public static void clear() {
        contextHolder.remove();
    }
}

4.动态数据源

创建一个动态数据源,继承自Spring的org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource类,并重写determineCurrentLookupKey方法

  • 该方法唯一需要做的事情就是获取我们在某一个SQL执行之前通过AOP切面动态指定的数据源名称。
public class DynamicDataSource extends AbstractRoutingDataSource {
    private final Logger log = LoggerFactory.getLogger(getClass());

    //缓存添加的数据源
    private Map<Object, Object> dynamicTargetDataSources = new HashMap<>();
    //缓存默认数据源
    private Object dynamicDefaultTargetDataSource = null;

    /**
     * 重新加载数据源
     */
    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        dynamicTargetDataSources.putAll(targetDataSources);
        super.afterPropertiesSet();// 必须添加该句,否则新添加数据源无法识别到
    }

    /**
     * 设置默认数据源
     */
    @Override
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        this.dynamicDefaultTargetDataSource = defaultTargetDataSource;
        super.afterPropertiesSet();// 必须添加该句,否则新添加数据源无法识别到
    }


    /**
     * 获取默认数据源
     */
    public Object getDynamicDefaultTargetDataSource() {
        return dynamicDefaultTargetDataSource;
    }

    /**
     * 获取已加载的数据源
     */
    public Map<Object, Object> getDynamicTargetDataSources() {
        return dynamicTargetDataSources;
    }

    /**
     * 重写至AbstractRoutingDataSource,运行时就是在这里根据名称动态切换数据源的
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.get();
    }
}

5.动态数据源配置类

解析在application.yml中配置的多个数据源,并初始化动态数据源实体类DynamicDataSource 以及sqlSessionFactory

@Configuration
//sqlSessionFactoryRef 表示定义了 key ,表示一个唯一 SqlSessionFactory 实例
@MapperScan(basePackages = {DynamicDataSourceConfiguration.MAPPER_INTERFACE_PACKAGE}, sqlSessionTemplateRef = "sqlSessionTemplate")
public class DynamicDataSourceConfiguration {
    /**
     * 扫描的mapper接口路径
     * 	注意:com.oyjp.ds3.mapper是我项目里的Mapper接口全路径(这里需要改成你们项目中的全路径)
     */
    public static final String MAPPER_INTERFACE_PACKAGE = "com.oyjp.ds3.mapper";
    /**
     * 扫描的mapper文件路径
     * 注意:classpath*:com/oyjp/ds3/mapper/*.xml是我项目里的xml的路径(这里需要改成你们项目中xml的路径)
     */
    public static final String MAPPER_XML_PACKAGE = "classpath*:com/oyjp/ds3/mapper/*.xml";


    //一个一个的将属性注入
    /*
    @Value("${spring.datasource.db01.url}")
    private String db01DBUrl;
    @Value("${spring.datasource.db01.username}")
    private String db01DBUser;
    @Value("${spring.datasource.db01.password}")
    private String db01DBPassword;
    @Value("${spring.datasource.db01.driverClassName}")
    private String db01DriverClassName;

    @Value("${spring.datasource.db02.url}")
    private String db02DBUrl;
    @Value("${spring.datasource.db02.username}")
    private String db02DBUser;
    @Value("${spring.datasource.db02.password}")
    private String db02DBPassword;
    @Value("${spring.datasource.db02.driverClassName}")
    private String db02DriverClassName;
    */

    /**
     * 初始化db01数据源
     */
    @Bean(name = "db01DataSource")
    //根据配置前缀将属性注入到对象属性中,数据源db01的配置前缀为spring.datasource.db01
    @ConfigurationProperties(prefix = "spring.datasource.db01")
    public DataSource db01DataSource() {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        dataSource.setName("db01");//数据源名称
        return dataSource;
    }

    /**
     * 初始化db02数据源
     */
    @Bean(name = "db02DataSource")
    //根据配置前缀将属性注入到对象属性中,数据源db01的配置前缀为spring.datasource.db02
    @ConfigurationProperties(prefix = "spring.datasource.db02")
    public DataSource db02DataSource() {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        dataSource.setName("db02");//数据源名称
        return dataSource;
    }

    /**
     * 初始化动态数据源,并注入db01,db02,以及设置默认数据源为db01
     */
    @Bean
    public DynamicDataSource dynamicDataSource(@Qualifier("db01DataSource") DataSource db01DataSource, @Qualifier("db02DataSource") DataSource db02DataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource;
        Map<Object, Object> map = new HashMap<>();
        map.put("db01", db01DataSource);
        map.put("db02", db02DataSource);
        //默认数据源
        dynamicDataSource.setDefaultTargetDataSource(db01DataSource);
        //数据源列表,与名称绑定
        dynamicDataSource.setTargetDataSources(map);

        return dynamicDataSource;
    }

   /**
     * 配置 SqlSessionFactoryBean
     * 将 MyBatis 的 mapper 位置和持久层接口的别名设置到 Bean 的属性中,如果没有使用 *.xml 则可以不用该配置,否则将会产生 invalid bond statement 异常
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
         // 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource 作为数据源则不能实现切换
        bean.setDataSource(dynamicDataSource);
         //此处设置为了解决找不到mapper文件的问题
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_XML_PACKAGE));
        return bean.getObject();
    }

    /**
     * 设置动态数据源DynamicDataSource到会话工厂
     */
    @Bean(name = "sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    /**
     * 将动态数据源添加到事务管理器中,并生成新的bean
     *
     * 切库与事务注意:
     *   1.有@Transactional注解的方法,方法内部不可以做切换数据库 操作
     *   2.在同一个service其他方法调用带@Transactional的方法,事务不起作用
     *   3.在应用中因为使用了 DAO 层的切面切换数据源,所以 @Transactional 注解不能加在类上,只能用于方法;有 @Trasactional注解的方法无法切换数据源
     * @return 事务管理实例
     */
    @Bean
    public PlatformTransactionManager platformTransactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }
}
  • dynamicDataSource.setDefaultTargetDataSource(db01DataSource); 这一行代码表示没有指定数据源时,默认使用默认数据源。

3.自定义注解

在实际的项目开发中,不可能总是在访问数据库之前,调用DataSourceContextHolder .set设置数据源,这样不好维护、繁琐、代码可阅读性也不好。所以,可以自定义一个注解,用于标识当前方法是要切换到数据源,然后用一个切面(AOP)在进入方法前指定数据源、退出方法前清空数据源。

import java.lang.annotation.*;
/**
 * 数据源注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface DS {
    /**
     * 没有设置数据源名称,默认取default名称数据源
     */
    String value() default "default";
}

7.动态数据源切面类(多数据源动态切换)

通过AOP+注解实现数据源的动态切换,在调用切入点下面的方法时,会先进入前置通知方法中,将注解@DS配置数据源名称设置到DataSourceContextHolder的ThreadLocal中,在方法返回前清空ThreadLocal设置的数据源

  • 其中@Order是很重要的,必须确保DynamicDataSourceAspect 的执行优先于TranctionInterceptor。不然数据源的指定就无法生效(数据源的指定在数据库连接的获取之后!!)
/**
 * 动态数据源AOP切面
 */
@Aspect
@Order(-1)
@Component
public class DynamicDataSourceAspect {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    //切点
    @Pointcut("execution(* com.oyjp.ds3.service..*(..))")
    public void aspect() {
    }

    //调用方法前结束后,根据注解@DS设置数据源
    @Before("aspect()")
    private void before(JoinPoint point) {
        Object target = point.getTarget();
        String method = point.getSignature().getName();
        Class<?> classz = target.getClass();// 获取目标类
        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
                .getMethod().getParameterTypes();
        try {
            Method m = classz.getMethod(method, parameterTypes);
            if (m != null && m.isAnnotationPresent(DS.class)) {
                DS data = m.getAnnotation(DS.class);
                logger.info("method :{},datasource:{}", m.getName(), data.value());
                DataSourceContextHolder.set(data.value());// 数据源放到当前线程中
            }
        } catch (Exception e) {
            logger.error("get datasource error ", e);
            //默认选择master
            DataSourceContextHolder.set("master");// 数据源放到当前线程中
        }
    }

    //调用方法结束后,清空数据源
    @AfterReturning("aspect()")
    public void after(JoinPoint point) {
        DataSourceContextHolder.clear();
    }
}

8.启动类取消自动配置数据源

//指定aop事务执行顺序,已保证在切换数据源的后面
@EnableTransactionManagement(order = 2)
//排除数据源自动配置
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class})

9.测试

在需要切换数据源的service的方法上面标注自定义注解@DS即可

mapper接口

@Mapper
public interface SysTaskMapper {
    /**
     * 通过ID查询单条数据
     *
     * @param id 主键
     * @return 实例对象
     */
    SysTask queryById(@Param("id") Integer id);
}

Mapper文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.oyjp.ds3.mapper.SysTaskMapper">

    <resultMap type="com.oyjp.ds3.bean.SysTask" id="SysTaskMap">
        <result property="id" column="id" jdbcType="INTEGER"/>
        <result property="jobName" column="job_name" jdbcType="VARCHAR"/>
        <result property="description" column="description" jdbcType="VARCHAR"/>
        <result property="cronExpression" column="cron_expression" jdbcType="VARCHAR"/>
        <result property="beanClass" column="bean_class" jdbcType="VARCHAR"/>
        <result property="jobStatus" column="job_status" jdbcType="VARCHAR"/>
        <result property="jobGroup" column="job_group" jdbcType="VARCHAR"/>
        <result property="createUser" column="create_user" jdbcType="VARCHAR"/>
        <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
        <result property="updateUser" column="update_user" jdbcType="VARCHAR"/>
        <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
    </resultMap>

    <!--查询单个-->
    <select id="queryById" resultMap="SysTaskMap">
        select   * from sys_task  where id = #{id}
    </select>
</mapper>

Service接口

public interface SysTaskService {
    /**
     * 查询db01数据源
     *
     * @param id
     * @return
     */
    SysTask queryById1(Integer id);

    /**
     * 查询db02数据源
     *
     * @param id
     * @return
     */
    SysTask queryById2(Integer id);

    /**
     * 查询db03数据源
     *
     * @param id
     * @return
     */
    SysTask queryById3(Integer id);
    /**
     * 通过ID查询单条数据
     *
     * @param id 主键
     * @return 实例对象
     */
    SysTask queryById(Integer id);

}

Service接口实现类

@Service("SysTaskService")
public class SysTaskServiceImpl implements SysTaskService {
    @Resource
    private SysTaskMapper SysTaskMapper;

    /**
     *查询单条数据源,没有指定数据源,查询默认数据db01的数据
     */
    @Override
    public SysTask queryById(Integer id) {
        return this.SysTaskMapper.queryById(id);
    }

    /**
     * 查询db01数据源原
     */
    @Override
    @DS("db01")
    public SysTask queryById1(Integer id) {
        return this.SysTaskMapper.queryById(id);
    }

    /**
     * 查询db02数据源原
     */
    @Override
    @DS("db02")
    public SysTask queryById2(Integer id) {
        return this.SysTaskMapper.queryById(id);
    }

    /**
     * 查询db03数据源原
     */
    @Override
    @DS("db03")
    public SysTask queryById3(Integer id) {
        return this.SysTaskMapper.queryById(id);
    }
}

伪代码-controller

@RestController
public class SysTaskController {
    Logger log = LoggerFactory.getLogger(SysTaskController.class);
    /**
     * 服务对象
     */
    @Resource
    private SysTaskService sysTaskService;

    /**
     * 切换到数据源db01
     */
    @GetMapping("/tasks1/{id}")
    public SysTask selectOne1(@PathVariable("id") Integer id) {
        return this.sysTaskService.queryById1(id);
    }

    /**
     * 切换到数据源db02
     */
    @GetMapping("/tasks2/{id}")
    public SysTask selectOne2(@PathVariable("id") Integer id) {
        return this.sysTaskService.queryById2(id);
    }

    /**
     * 切换到数据源db03,没有配置此数据源,因此默认查询的是db01的数据
     */
    @GetMapping("/tasks3/{id}")
    public SysTask selectOne3(@PathVariable("id") Integer id) {
        return this.sysTaskService.queryById3(id);
    }
 }

也可以在方法内通过DataSourceContextHolder.set()方法修改数据源名称,从而在进行数据源操作时触发DynamicDataSource.determineCurrentLookupKey获取到我们修改后的数据源名称

/**
     * 切换到db01数据源
     */
    @GetMapping("/tasks4/{id}")
    public SysTask selectOne4(@PathVariable("id") Integer id) {
        DataSourceContextHolder.set("db01");
        return this.sysTaskService.queryById(id);
    }

    /**
     * 切换到db02数据源
     */
    @GetMapping("/tasks5/{id}")
    public SysTask selectOne5(@PathVariable("id") Integer id) {
        DataSourceContextHolder.set("db02");
        return this.sysTaskService.queryById(id);
    }

    /**
     * 切换到db03数据源,没有配置此数据源,因此默认查询的是db01的数据
     */
    @GetMapping("/tasks6/{id}")
    public SysTask selectOne6(@PathVariable("id") Integer id) {
        DataSourceContextHolder.set("db03");
        return this.sysTaskService.queryById(id);
    }

10.动态加载数据源

基于上面的代码进行新增

10.1.新增获取Spring容器工具类
  • 主要是用于获取Spring容器中的动态数据源DynamicDataSource 实例
/**
 * 实现ApplicationContextAware接口设置applicationContext
 * 提供static方法供调用者使用,不要求使用者受spring容器管理
 */
@Component
public class SpringContext implements ApplicationContextAware {

    public static ApplicationContext applicationContext;

    public static ApplicationContext getInstance() {
        return SpringContext.applicationContext;
    }

    public static Object getBean(String name) {
        return getInstance().getBean(name);
    }

    public static <T> T getBean(Class<T> clazz) {
        return getInstance().getBean(clazz);
    }

    public static <T> T getBean(String name, Class<T> clazz) {
        return getInstance().getBean(name, clazz);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContext.applicationContext = applicationContext;
    }
}
10.2.动态数据源服务类

动态数据源服务类,提供一些动态添加数据源、删除数据源方法

@Service
public class DynamicDataSourceService {
    private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceService.class);
    //可重入锁
    private final Lock lock = new ReentrantLock();

    /**
     * 删除数据源
     *
     * @param dsName 数据源名称
     * @return
     */
    public boolean delDataSources(String dsName) {
        lock.lock();
        try {
            //获取Spring容器动态数据源对象
            DynamicDataSource dynamicDataSource = SpringContext.getInstance().getBean(DynamicDataSource.class);
            //获取已加载的所有数据源
            Map<Object, Object> dynamicTargetDataSources = dynamicDataSource.getDynamicTargetDataSources();

            //返回结果
            boolean result = false;
            //如果存在当前数据源
            if (dynamicTargetDataSources.containsKey(dsName)) {
                //获取Druid管理下的数据源
                Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
                for (DruidDataSource druidDataSource : druidDataSourceInstances) {
                    if (druidDataSource.getName().equals(dsName)) {
                        //删除数据源监控列表
                        DruidDataSourceStatManager.removeDataSource(druidDataSource);
                        //删除数据源
                        dynamicTargetDataSources.remove(dsName);
                        //将删除后的数据源集合赋值给父类的TargetDataSources
                        dynamicDataSource.setTargetDataSources(dynamicTargetDataSources);
                        log.info(dsName + "数据源删除成功");
                        result = true;
                        break;
                    }
                }
            }
            return result;
        } finally {
            lock.unlock();
        }
    }


    /**
     * 创建Druid数据源
     *
     * @param dsName     数据源名称
     * @param driveClass 数据源驱动
     * @param url        数据库url
     * @param username   用户名
     * @param password   密码
     * @return
     */
    public boolean addDataSource(String dsName, String driveClass, String url, String username, String password) {
        lock.lock();
        try {
            //测试当前连接
            if (!testDatasource(dsName, driveClass, url, username, password)) return false;

            //获取Spring容器动态数据源对象
            DynamicDataSource dynamicDataSource = SpringContext.getInstance().getBean(DynamicDataSource.class);
            //获取已加载的所有数据源
            Map<Object, Object> dynamicTargetDataSources = dynamicDataSource.getDynamicTargetDataSources();

            //创建Druid数据源
            DruidDataSource druidDataSource = new DruidDataSource();
            druidDataSource.setName(dsName);
            druidDataSource.setDriverClassName(driveClass);
            druidDataSource.setUrl(url);
            druidDataSource.setUsername(username);
            druidDataSource.setPassword(password);
            druidDataSource.init();

            //添加数据源
            dynamicTargetDataSources.put(dsName, druidDataSource);
            //将删除后的数据源集合赋值给父类的TargetDataSources
            dynamicDataSource.setTargetDataSources(dynamicTargetDataSources);
            log.info(dsName + "数据源初始化成功");
            log.info(dsName + "数据源的概况:" + druidDataSource.dump());
            return true;

        } catch (Exception e) {
            log.error(dsName + "数据源初始化异常:{}",e.getMessage());
            return false;
        } finally {
            lock.unlock();
        }
    }


    /**
     * 测试数据源是否能够连接
     *
     * @param dsName     数据源名称
     * @param driveClass 驱动
     * @param url        url
     * @param username   账号
     * @param password   密码
     * @return true可以 false不可以
     */
    public boolean testDatasource(String dsName, String driveClass, String url, String username, String password) {
        try {
            Class.forName(driveClass);
            //设置连接数据库的等待时间,单位秒
            DriverManager.setLoginTimeout(3);
            DriverManager.getConnection(url, username, password);
            log.info(dsName + "数据源建立连接正常");
            return true;
        } catch (Exception e) {
            log.info(dsName + "数据源建立连接异常:{}", e.getMessage());
            return false;
        }
    }
}
10.4.测试
@RestController
public class SysTaskController {
    Logger log = LoggerFactory.getLogger(SysTaskController.class);
    /**
     * 服务对象
     */
    @Resource
    private SysTaskService sysTaskService;
    @Resource
    private DBService dbService;
    @Resource
    private DynamicDataSourceService dynamicDataSourceService;

    /**
     * 切换到数据源db01-aop
     *
     * @param id 主键
     * @return 单条数据
     */
    @GetMapping("/tasks1/{id}")
    public SysTask selectOne1(@PathVariable("id") Integer id) {
        return this.sysTaskService.queryById1(id);
    }

    /**
     * 切换到数据源db02-aop
     *
     * @param id 主键
     * @return 单条数据
     */
    @GetMapping("/tasks2/{id}")
    public SysTask selectOne2(@PathVariable("id") Integer id) {
        return this.sysTaskService.queryById2(id);
    }


    /**
     * 切换到数据源db02-aop
     *
     * @param id 主键
     * @return 单条数据
     */
    @GetMapping("/tasks3/{id}")
    public SysTask selectOne3(@PathVariable("id") Integer id) {
        return this.sysTaskService.queryById3(id);
    }

    /**
     * 动态加载数据源
     */
    @GetMapping("/tasks/addDataSource")
    public String addDs() {
        String driverClassName = "com.mysql.jdbc.Driver";
        String url = "jdbc:mysql://127.0.0.1:3306/springboot_quertz3?characterEncoding=utf-8&serverTimezone=UTC";
        String username = "root";
        String password = "root";
        String dbName = "db03";

        //动态加载数据源
        if (dynamicDataSourceService.addDataSource(dbName, driverClassName, url, username, password)) {
            return "success";
        }
        return "fail";
    }


    /**
     * 动态加载数据源
     */
    @GetMapping("/tasks/delDataSource/{dsName}")
    public String delDs(@PathVariable("dsName") String dsName) {
        //删除数据源
        if (dynamicDataSourceService.delDataSources(dsName)) {
            return "success";
        }
        return "fail";
    }
}

11. 关于事务

11.1.单个数据源测试

AbstractRoutingDataSource 只支持单库事务,也就是说切换数据源要在开启事务之前执行

  • Spring DataSourceTransactionManager进行事务管理,开启事务,会将数据源缓存到DataSourceTransactionObject对象中进行后续的commit rollback等事务操作。

出现多数据源动态切换失败的原因是因为在事务开启后,数据源就不能再进行随意切换了,也就是说,一个事务对应一个数据源

  • 传统的Spring管理事务是放在·Service业务层操作的,所以更换数据源的操作要放在这个操作之前进行。也就是切换数据源操作放在Controller层,可是这样操作会造成Controller层代码混乱的结果。
  • 故而想到的解决方案是将事务管理在数据持久 (Dao层) 开启,切换数据源的操作放在业务层进行操作,就可在事务开启之前顺利进行数据源切换,不会再出现切换失败了。