多租户动态数据源插件dynamic-datasource简介

一、简介

dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。

由MyBatisPlus的团队baomidou开发,其支持 Jdk 1.7+, SpringBoot 1.5.x 2.x.x 3.x.x。

JPA用户不建议使用,JPA自带事务,无法连续切库。

二、特性
  • 支持数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
  • 支持数据库敏感配置信息加密(可自定义) ENC()。
  • 支持每个数据库独立初始化表结构schema和数据库database。
  • 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
  • 支持自定义注解 ,需继承DS(3.2.0+)。
  • 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
  • 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
  • 提供 自定义数据源来源 方案(如全从数据库加载)。
  • 提供项目启动后 动态增加移除数据源 方案。
  • 提供Mybatis环境下的 纯读写分离 方案。
  • 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
  • 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
  • 提供 基于seata的分布式事务方案 。
  • 提供 本地多数据源事务方案。
三、约定
  1. 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
  2. 配置文件所有以下划线 _ 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。
  3. 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
  4. 默认的数据源名称为 master ,你可以通过 spring.datasource.dynamic.primary 修改。
  5. 方法上的注解优先于类上注解。
  6. DS支持继承抽象类上的DS,暂不支持继承接口上的DS。
四、基础使用
  1. 引入dynamic-datasource-spring-boot-starter。

spring-boot 1.5.x 或 2.x.x

xml 复制代码
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>${version}</version>
</dependency>

spring-boot 3

xml 复制代码
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
  <version>${version}</version>
</dependency>
  1. 配置数据源。
yaml 复制代码
spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        slave_1:
          url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        slave_2:
          url: ENC(xxxxx) # 内置加密,使用请查看详细文档
          username: ENC(xxxxx)
          password: ENC(xxxxx)
          driver-class-name: com.mysql.jdbc.Driver
       #......省略
       #以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
yaml 复制代码
# 多主多从                      纯粹多库(记得设置primary)                   混合配置
spring:                               spring:                               spring:
  datasource:                           datasource:                           datasource:
    dynamic:                              dynamic:                              dynamic:
      datasource:                           datasource:                           datasource:
        master_1:                             mysql:                                master:
        master_2:                             oracle:                               slave_1:
        slave_1:                              sqlserver:                            slave_2:
        slave_2:                              postgresql:                           oracle_1:
        slave_3:                              h2:                                   oracle_2:

3.使用 @DS 切换数据源。

@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。

注解 结果
没有@DS 默认数据源
@DS("dsName") dsName可以为组名也可以为具体某个库的名称
java 复制代码
@Service
@DS("slave")
public class UserServiceImpl implements UserService {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  public List selectAll() {
    return  jdbcTemplate.queryForList("select * from user");
  }
  
  @Override
  @DS("slave_1")
  public List selectByCondition() {
    return  jdbcTemplate.queryForList("select * from user where age >10");
  }
}
  1. @DS注解的使用位置

其实没有一定的要求,看实际情况:

  • DS注解是基于AOP的原理实现的,aop的常见失效场景应清楚。 比如内部调用失效,shiro代理失效等。
  • 通常建议DS放在serviceImpl的方法上,如事务注解一样。
  • 可以注解在Controller的方法上或类上,但不建议,原因是controller的操作常常并不涉及数据库。
  • 注解在mapper上,如果你某个Mapper对应的表只在确定的一个库,也是可以的。 但是建议只注解在Mapper的类上。
  • 支持继承抽象类上的DS注解
  • 支持继承接口上的DS(3.4.1开始支持),但是一个类能实现多个接口,如果多个接口都有DS会有问题。
五、连接池配置

每个数据源都有一个type来指定连接池。

每个数据源甚至可以使用不同的连接池,如无特殊需要并不建议。

type 不是必填字段 ,在没有设置type的时候系统会自动按以下顺序查找并使用连接池
Druid>HikariCp>BeeCp>DBCP2>Spring Basic

yaml 复制代码
spring:
  datasource:
    dynamic:
      primary: db1 
      datasource:
        db1:
          url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
          type: com.zaxxer.hikari.HikariDataSource #使用Hikaricp
        db2:
          url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
          type: com.alibaba.druid.pool.DruidDataSource #使用Druid
        db3:
          url: jdbc:mysql://xx.xx.xx.xx:3308/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
          type: cn.beecp.BeeDataSource #使用beecp

集成连接池(以Druid为例)

  1. 项目引入 druid-spring-boot-starter 依赖
xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>${version}</version>
</dependency>
  1. 排除原生Druid的快速配置类。
java 复制代码
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
public class Application {

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

某些springBoot的版本上面可能无法排除可用以下方式排除。

yaml 复制代码
spring:
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
  1. 参数配置

如果参数都未配置,则保持原组件默认值。

如果配置了全局参数,则每一个数据源都会继承对应参数。

每一个数据源可以单独设置参数覆盖全局参数。

yaml 复制代码
spring:
  datasource:
    druid:
      stat-view-servlet:
        enabled: true
        loginUsername: admin
        loginPassword: 123456
    dynamic:
      druid: #以下是支持的全局默认值
        initial-size:
        max-active:
        filters: stat # 注意这个值和druid原生不一致,默认启动了stat。 如果确定什么filter都不需要 这里填 ""
        ...等等基本都支持
        wall:
            none-base-statement-allow:
            ...等等
        stat:
          merge-sql:
          log-slow-sql:
          slow-sql-millis: 
      datasource:
        master:
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic?characterEncoding=utf8&useSSL=false
          druid: # 以下是独立参数,每个库可以重新设置
            initial-size: 20
            validation-query: select 1 FROM DUAL #比如oracle就需要重新设置这个
            public-key: #(非全局参数)设置即表示启用加密,底层会自动帮你配置相关的连接参数和filter,推荐使用本项目自带的加密方法。
#           ......

# 生成 publickey 和密码,推荐使用本项目自带的加密方法。
# java -cp druid-1.1.10.jar com.alibaba.druid.filter.config.ConfigTools youpassword
六、三方集成
  1. 集成MybatisPlus

只要引入了mybatisPlus相关jar包,项目自动集成,兼容mybatisPlus 2.x和3.x的版本

  • 分页配置
java 复制代码
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    //如果是不同类型的库,请不要指定DbType,其会自动判断。
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
   // interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
}
  • 注意事项
    mp内置的ServiceImpl在新增,更改,删除等一些方法上自带事物导致不能切换数据源。
    解决办法:
    (1)复制ServiceImpl出来为自己的MyServiceImpl,并去掉所有事务注解。
    (2)创建一个新方法,内部调用Mapper,并在此方法上添加DS注解。
  1. 集成P6spy
yaml 复制代码
spring:
  datasource:
    dynamic:
      p6spy: true # 默认false,建议线上关闭。
      datasource:
        product:
          username: sa
          password: ""
          url: jdbc:h2:mem:test
          driver-class-name: org.h2.Driver
          p6spy: false # 如果这个库不需要可单独关闭。
        order:
          username: sa
          password: ""
          url: jdbc:h2:mem:test
          driver-class-name: org.h2.Driver
七、动态添加移除数据源

主要在多租户场景中,一个新的租户进来需要动态的添加一个数据源到库中,使得系统不用重启即可切换数据源。

java 复制代码
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.*;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.samples.ds.dto.DataSourceDTO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.sql.DataSource;
import java.util.Set;

@RestController
@RequestMapping("/datasources")
@Api(tags = "添加删除数据源")
public class DataSourceController {

    @Autowired
    private DataSource dataSource;
   // private final DataSourceCreator dataSourceCreator; //3.3.1及以下版本使用这个通用,强烈推荐sb2用户至少升级到3.5.2版本

    @Autowired
    private DefaultDataSourceCreator dataSourceCreator;
//如果是用4.x以上版本,因为要和spring解绑,重构了一些东西,比如缺少了懒启动和启动初始化数据库。不太建议用以下独立的创建器,只建议用上面的DefaultDataSourceCreator 
    @Autowired
    private BasicDataSourceCreator basicDataSourceCreator;
    @Autowired
    private JndiDataSourceCreator jndiDataSourceCreator;
    @Autowired
    private DruidDataSourceCreator druidDataSourceCreator;
    @Autowired
    private HikariDataSourceCreator hikariDataSourceCreator;
    @Autowired
    private BeeCpDataSourceCreator beeCpDataSourceCreator;
    @Autowired
    private Dbcp2DataSourceCreator dbcp2DataSourceCreator;

    @GetMapping
    @ApiOperation("获取当前所有数据源")
    public Set<String> now() {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        return ds.getDataSources().keySet();
    }

    //通用数据源会根据maven中配置的连接池根据顺序依次选择。
    //默认的顺序为druid>hikaricp>beecp>dbcp>spring basic
    @PostMapping("/add")
    @ApiOperation("通用添加数据源(推荐)")
    public Set<String> add(@Validated @RequestBody DataSourceDTO dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
        return ds.getDataSources().keySet();
    }

    @PostMapping("/addBasic(强烈不推荐,除了用了马上移除)")
    @ApiOperation(value = "添加基础数据源", notes = "调用Springboot内置方法创建数据源,兼容1,2")
    public Set<String> addBasic(@Validated @RequestBody DataSourceDTO dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = basicDataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
        return ds.getDataSources().keySet();
    }

    @PostMapping("/addJndi")
    @ApiOperation("添加JNDI数据源")
    public Set<String> addJndi(String pollName, String jndiName) {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = jndiDataSourceCreator.createDataSource(jndiName);
        ds.addDataSource(poolName, dataSource);
        return ds.getDataSources().keySet();
    }

    @PostMapping("/addDruid")
    @ApiOperation("基础Druid数据源")
    public Set<String> addDruid(@Validated @RequestBody DataSourceDTO dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        dataSourceProperty.setLazy(true);
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = druidDataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
        return ds.getDataSources().keySet();
    }

    @PostMapping("/addHikariCP")
    @ApiOperation("基础HikariCP数据源")
    public Set<String> addHikariCP(@Validated @RequestBody DataSourceDTO dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = hikariDataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
        return ds.getDataSources().keySet();
    }

    @PostMapping("/addBeeCp")
    @ApiOperation("基础BeeCp数据源")
    public Set<String> addBeeCp(@Validated @RequestBody DataSourceDTO dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = beeCpDataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
        return ds.getDataSources().keySet();
    }

    @PostMapping("/addDbcp")
    @ApiOperation("基础Dbcp数据源")
    public Set<String> addDbcp(@Validated @RequestBody DataSourceDTO dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = dbcp2DataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
        return ds.getDataSources().keySet();
    }

    @DeleteMapping
    @ApiOperation("删除数据源")
    public String remove(String name) {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        ds.removeDataSource(name);
        return "删除成功";
    }
}
八、动态解析数据源

默认有三个职责链来处理动态参数解析器 header->session->spel

所有以 # 开头的参数都会从参数中获取数据源。

java 复制代码
@DS("#session.tenantName")//从session获取
public List selectSpelBySession() {
	return userMapper.selectUsers();
}

@DS("#header.tenantName")//从header获取
public List selectSpelByHeader() {
	return userMapper.selectUsers();
}

@DS("#tenantName")//使用spel从参数获取
public List selectSpelByKey(String tenantName) {
	return userMapper.selectUsers();
}

@DS("#user.tenantName")//使用spel从复杂参数获取
public List selecSpelByTenant(User user) {
	return userMapper.selectUsers();
}
  • 自定义参数解析
    参考header解析器,继承DsProcessor,如果matches返回true则匹配成功,调用doDetermineDatasource返回匹配到的数据源,否则跳到下一个解析器.
java 复制代码
public class DsHeaderProcessor extends DsProcessor {
    //前缀
    private static final String HEADER_PREFIX = "#header";

    @Override
    public boolean matches(String key) {
        return key.startsWith(HEADER_PREFIX);
    }

    @Override
    public String doDetermineDatasource(MethodInvocation invocation, String key) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        //获取请求头中key为"xxx"的值,完整表达式是"#header.xxx"
        return request.getHeader(key.substring(HEADER_PREFIX.length()+1)); 
    }
}

重写完后重新注入一个根据自己解析顺序的解析处理器

java 复制代码
@Configuration
public class MyDynamicDataSourceConfig{

   @Bean
   public DsProcessor dsProcessor() {
        DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
        DsSessionProcessor sessionProcessor = new DsSessionProcessor();
        DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
        headerProcessor.setNextProcessor(sessionProcessor);
        sessionProcessor.setNextProcessor(spelExpressionProcessor);
        return headerProcessor;
   }
}

注意:如果在Mapper接口下面的方法使用

java 复制代码
public interface UserMapper{
    // 前缀可以是p0,a0
    @DS("#p0.tenantName")
    public List selecSpelByTenant(User user);
}

spel中只能使用a或p带下标指定参数, 如 a0, p0, a1, p1,...

原因是编译器的默认配置没有将接口参数的元数据写入字节码文件中.

如果要使用参数名须在maven中配置编译接口参数信息写入字节码,并在编译带上-parameters

xml 复制代码
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <!-- 想启用  <parameters>true</parameters> 的maven编译最低版本为:3.6.2 -->
        <version>3.6.2</version>
        <configuration>
            <source>${java.version}</source>
            <target>${java.version}</target>
            <parameters>true</parameters>
        </configuration>
    </plugin>

idea java编译配置: -parameters

九、配置加密

在一些项目中,有对数据库关键字段加密的需求。

本项目也支持支持url , username, password 的加密。

使用的RAS加密,私钥加密,公钥解密。

  • 私钥加密,公钥解密
java 复制代码
import com.baomidou.dynamic.datasource.toolkit.CryptoUtils;

public class Demo {
        
    public static void main(String[] args) throws Exception {
        String password = "123456";
        //使用默认的密钥对加解密(不推荐)
        String encodePassword = CryptoUtils.encrypt(password);
        System.out.println(encodePassword);

        //使用自定义密钥对加解密(推荐)
        String[] arr = CryptoUtils.genKeyPair(512);
        //私钥用于加密,请自行保存,不要保存在项目代码或配置文件里
        System.out.println("privateKey:  " + arr[0]); 
        //公钥用于解密,需配置到yml中,生产环境配置可交给运维在启动时传入,避免暴露给开发人员
        System.out.println("publicKey:  " + arr[1]); 
        //分别对以下值加密得到密文,然后配置到配置文件中: ENC(密文)
        System.out.println("url:  " + CryptoUtils.encrypt(arr[0], "jdbc:mysql://127.0.0.1:3306/order"));
        System.out.println("username:  " + CryptoUtils.encrypt(arr[0], "root"));
        System.out.println("password:  " + CryptoUtils.encrypt(arr[0], "123456"));
    }
}
  • 配置加密yml
    ENC(xxx)` 中包裹的xxx即为使用加密方法后生成的字符串。
yaml 复制代码
spring:
  datasource:
    dynamic:
      public-key: #有默认值,强烈建议更换
      datasource:
        master:
          url: ENC(xxxxx)
          username: ENC(xxxxx)
          password: ENC(xxxxx)
          driver-class-name: com.mysql.jdbc.Driver
          public-key: #每个数据源可以独立设置,没有就继承上面的。
  • 自定义解密
    如果想使用自己的方式加密,解密。从3.5.0版本开始,扩展了一个event。用户自行实现注入即可。
java 复制代码
public interface DataSourceInitEvent {

    /**
     * 连接池创建前执行(可用于参数解密)
     *
     * @param dataSourceProperty 数据源基础信息
     */
    void beforeCreate(DataSourceProperty dataSourceProperty);

    /**
     * 连接池创建后执行
     *
     * @param dataSource 连接池
     */
    void afterCreate(DataSource dataSource);
}

默认的实现如下,可参考

java 复制代码
package com.baomidou.dynamic.datasource.event;

import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.toolkit.CryptoUtils;
import com.baomidou.dynamic.datasource.toolkit.DsStrUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EncDataSourceInitEvent implements DataSourceInitEvent {
    private static final Logger log = LoggerFactory.getLogger(EncDataSourceInitEvent.class);
    private static final Pattern ENC_PATTERN = Pattern.compile("^ENC\\((.*)\\)$");

    public void beforeCreate(DataSourceProperty dataSourceProperty) {
        String publicKey = dataSourceProperty.getPublicKey();
        if (DsStrUtils.hasText(publicKey)) {
            dataSourceProperty.setUrl(this.decrypt(publicKey, dataSourceProperty.getUrl()));
            dataSourceProperty.setUsername(this.decrypt(publicKey, dataSourceProperty.getUsername()));
            dataSourceProperty.setPassword(this.decrypt(publicKey, dataSourceProperty.getPassword()));
        }

    }

    public void afterCreate(DataSource dataSource) {
    }

    private String decrypt(String publicKey, String cipherText) {
        if (DsStrUtils.hasText(cipherText)) {
            Matcher matcher = ENC_PATTERN.matcher(cipherText);
            if (matcher.find()) {
                try {
                    return CryptoUtils.decrypt(publicKey, matcher.group(1));
                } catch (Exception e) {
                    log.error("DynamicDataSourceProperties.decrypt error ", e);
                }
            }
        }

        return cipherText;
    }
}
十、启动初始化执行脚本

3.5.0 之前版本

yaml 复制代码
spring:
  datasource:
    dynamic:
      primary: order
      datasource:
        order:
          # 基础配置省略...
          schema: db/order/schema.sql # 配置则生效,自动初始化表结构
          data: db/order/data.sql # 配置则生效,自动初始化数据
          continue-on-error: true # 默认true,初始化失败是否继续
          separator: ";" # sql默认分号分隔符,一般无需更改
        product:
          schema: classpath*:db/product/schema.sql
          data: classpath*:db/product/data.sql
        user:
          schema: classpath*:db/user/schema/**/*.sql
          data: classpath*:db/user/data/**/*.sql

3.5.0(含) 之后版本 (层级多了一层init,保持和新版springboot一致)

yaml 复制代码
spring:
  datasource:
    dynamic:
      primary: order
      datasource:
        order:
          # 基础配置省略...
          init:
              schema: db/order/schema.sql # 配置则生效,自动初始化表结构
              data: db/order/data.sql # 配置则生效,自动初始化数据
              continue-on-error: true # 默认true,初始化失败是否继续
              separator: ";" # sql默认分号分隔符,一般无需更改
十一、无数据源启动

场景:部分系统可能启动的时候完全不需要数据源,全靠启动后动态添加。

  • 配置方式
    即基本不需要配置,可能根据需要配置一些类似Druid和HikariCp全局参数。
yaml 复制代码
spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.

一般无数据源启动需要结合【动态添加移除数据源】在启动后添加

十二、手动切换数据源
java 复制代码
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;

@Service
public class UserServiceImpl implements UserService {
  @Autowired
  private JdbcTemplate jdbcTemplate;

  public List selectAll() {
    DynamicDataSourceContextHolder.push("slave");//手动切换
    return  jdbcTemplate.queryForList("select * from user");
  }
 
}

需要注意的是手动切换的数据源,最好自己在合适的位置 调用DynamicDataSourceContextHolder.clear()清空当前线程的数据源信息

java 复制代码
@Slf4j
public class DynamicDatasourceClearInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
//入口处清空看个人,有的新人在异步里切了数据源但是忘记清除了,下一个请求遇到这个线程就会带进来。
        //DynamicDataSourceContextHolder.clear(); 
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        DynamicDataSourceContextHolder.clear();
    }

}
十三、自定义注解

如果你只有几个确定的库,可以尝试自定义注解替换掉@DS。

建议从3.2.1版本开始使用自定义注解,另外组件自带了@Master和@Slave注解。

  1. 需要自己定义一个注解并继承自DS。
java 复制代码
import com.baomidou.dynamic.datasource.annotation.DS;
import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@DS("product")
public @interface DSProduct {
}
  1. 注解在需要切换数据源的方法上或类上。
java 复制代码
@Service
@DSProduct
public class ProductServiceImpl implements ProductService {

    @DSProduct
    public List selectAll() {
      return  jdbcTemplate.queryForList("select * from products");
    }
}
十四、自定义数据源来源

数据源来源的默认实现是YmlDynamicDataSourceProvider,其从yaml或properties中读取信息并解析出所有数据源信息。

java 复制代码
public interface DynamicDataSourceProvider {

    /**
     * 加载所有数据源
     *
     * @return 所有数据源,key为数据源名称
     */
    Map<String, DataSource> loadDataSources();
}

自定义来源,可以参考 AbstractJdbcDataSourceProvider (仅供参考)来实现从JDBC数据库中获取数据库连接信息。

java 复制代码
@Bean
public DynamicDataSourceProvider jdbcDynamicDataSourceProvider() {
    return new AbstractJdbcDataSourceProvider("org.h2.Driver", "jdbc:h2:mem:test", "sa", "") {
        @Override
        protected Map<String, DataSourceProperty> executeStmt(Statement statement) throws SQLException {
            ResultSet rs = statement.executeQuery("select * from DB");
            while (rs.next()) {
                String name = rs.getString("name");
                String username = rs.getString("username");
                String password = rs.getString("password");
                String url = rs.getString("url");
                String driver = rs.getString("driver");
                DataSourceProperty property = new DataSourceProperty();
                property.setUsername(username);
                property.setPassword(password);
                property.setUrl(url);
                property.setDriverClassName(driver);
                map.put(name, property);
            }
            return map;
        }
    };
}

PS: 从3.4.0开始,可以注入多个DynamicDataSourceProvider的Bean以实现同时从多个不同来源加载数据源,注意同名会被覆盖

十五、自定义负载均衡策略

如下图slave组下有三个数据源,当用户使用slave切换数据源时会使用负载均衡算法。

系统自带了两个负载均衡算法

  • LoadBalanceDynamicDataSourceStrategy 轮询,是默认的。
  • RandomDynamicDataSourceStrategy 随机的
yaml 复制代码
spring:
  datasource:
    dynamic:
      datasource:
        master:
          username: sa
          password: ""
          url: jdbc:h2:mem:test
          driver-class-name: org.h2.Driver
          schema: db/schema.sql
        slave_1:
          username: sa
          password: ""
          url: jdbc:h2:mem:test
          driver-class-name: org.h2.Driver
        slave_2:
          username: sa
          password: ""
          url: jdbc:h2:mem:test
          driver-class-name: org.h2.Driver
        slave_3:
          username: sa
          password: ""
          url: jdbc:h2:mem:test
          driver-class-name: org.h2.Driver
      strategy: com.baomidou.dynamic.datasource.strategy.LoadBalanceDynamicDataSourceStrategy

如果默认的两个都不能满足要求,可以参考源码自定义。 暂时只能全局更改

java 复制代码
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import javax.sql.DataSource;

public class RandomDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
    public RandomDynamicDataSourceStrategy() {
    }

    public DataSource determineDataSource(List<DataSource> dataSources) {
        return (DataSource)dataSources.get(ThreadLocalRandom.current().nextInt(dataSources.size()));
    }
}
十六、动态路由数据源(无注解)
  • 本插件多数据源实现的核心
java 复制代码
public class DynamicRoutingDataSource {
@Override
public DataSource determineDataSource() {
    //这里是核心,是从ThreadLocal中获取当前数据源。
    String dsKey = DynamicDataSourceContextHolder.peek();
    return getDataSource(dsKey);
}
  • 所以我们就可以根据需求,选择合适的时机调用DynamicDataSourceContextHolder.push("对应数据源")。
  • 默认的@DS注解来切换数据源是根据spring AOP的特性,在方法开启前设置数据源KEY,方法执行完成后移除对应数据源KEY。
  • 在intercepror中切换(也可以在filter、aop中,可参考切换逻辑)
java 复制代码
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class DynamicDatasourceInterceptor implements HandlerInterceptor {

    /**
     * 在请求处理之前进行调用(Controller方法调用之前)
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String requestURI = request.getRequestURI();
        log.info("经过多数据源Interceptor,当前路径是{}", requestURI);
//        String headerDs = request.getHeader("ds");
//        Object sessionDs = request.getSession().getAttribute("ds");
        String s = requestURI.replaceFirst("/interceptor/", "");

        String dsKey = "master";
        if (s.startsWith("a")) {
            dsKey = "db1";
        } else if (s.startsWith("b")) {
            dsKey = "db2";
        } else {

        }

        DynamicDataSourceContextHolder.push(dsKey);
        return true;
    }

    /**
     * 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {

    }

    /**
     * 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        DynamicDataSourceContextHolder.clear();
    }

}
java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MyWebAutoConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new DynamicDatasourceInterceptor()).addPathPatterns("/interceptor/**");
    }
}

注意 : 此处是选择一个数据源,不会创建数据源,数据源需要在启动时全部创建好或者动态添加。

可以参考【自定义数据源来源】或者参考【动态添加移除数据源】

也可以在启动时手动查询租户连接信息创建对应租户数据源,参考代码如下:

java 复制代码
import com.saasbase.entity.Tenant;
import com.saasbase.mapper.TenantMapper;
import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;

@Slf4j
@Component
@AllArgsConstructor
public class DataSourceLoader {
    private final TenantMapper tenantMapper; // 查库
    private final DynamicDataSourceManager dsManager;

    @PostConstruct
    public void init() {
        //查询所有租户的连接信息
        List<Tenant> tenantList = tenantMapper.selectList(null);
        for (Tenant tenant : tenantList) {
            //数据源添加
            addTenantDataSource(
                    tenant.getId(),
                    tenant.getDbName(),
                    //todo 对user和password进行加解密,提高安全性
                    tenant.getDbUsername(),
                    tenant.getDbPassword(),
                    tenant.getDataSet() //使用默认前缀
            );
        }
    }
}
java 复制代码
public void addTenantDataSource(Long tenantId, String dbName, String dbUser, String dbPass, String urlPrefix) {

    DataSourceProperty property = new DataSourceProperty();
    String dsKey = "tenant_" + tenantId;

    property.setPoolName(dsKey);
    // 使用 @Value 注入的属性
    property.setDriverClassName(tenantDriverClassName);
    property.setUrl(urlPrefix + "/" + dbName + tenantUrlSuffix);
    property.setUsername(dbUser);
    property.setPassword(dbPass);

    // 应用 Hikari 连接池配置
    property.setHikari(hikariCpConfig);

    DataSource newDs = dataSourceCreator.createDataSource(property);
    DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
    ds.addDataSource(dsKey, newDs);
}
十七、部分源码(version 4.3.1)
  • DynamicRoutingDataSource (动态数据源管理)
java 复制代码
package com.baomidou.dynamic.datasource;

import com.baomidou.dynamic.datasource.destroyer.DataSourceDestroyer;
import com.baomidou.dynamic.datasource.destroyer.DefaultDataSourceDestroyer;
import com.baomidou.dynamic.datasource.ds.AbstractRoutingDataSource;
import com.baomidou.dynamic.datasource.ds.GroupDataSource;
import com.baomidou.dynamic.datasource.ds.ItemDataSource;
import com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException;
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import com.baomidou.dynamic.datasource.strategy.DynamicDataSourceStrategy;
import com.baomidou.dynamic.datasource.strategy.LoadBalanceDynamicDataSourceStrategy;
import com.baomidou.dynamic.datasource.toolkit.DsStrUtils;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.p6spy.engine.spy.P6DataSource;
import io.seata.rm.datasource.DataSourceProxy;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
    private static final Logger log = LoggerFactory.getLogger(DynamicRoutingDataSource.class);
    private static final String UNDERLINE = "_";
    private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap();
    private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap();
    private final List<DynamicDataSourceProvider> providers;
    private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
    private String primary = "master";
    private Boolean strict = false;
    private Boolean p6spy = false;
    private Boolean seata = false;
    private Boolean graceDestroy = false;

    public DynamicRoutingDataSource(List<DynamicDataSourceProvider> providers) {
        this.providers = providers;
    }

    protected String getPrimary() {
        return this.primary;
    }

    public DataSource determineDataSource() {
        String dsKey = DynamicDataSourceContextHolder.peek();
        return this.getDataSource(dsKey);
    }

    private DataSource determinePrimaryDataSource() {
        log.debug("dynamic-datasource switch to the primary datasource");
        DataSource dataSource = (DataSource)this.dataSourceMap.get(this.primary);
        if (dataSource != null) {
            return dataSource;
        } else {
            GroupDataSource groupDataSource = (GroupDataSource)this.groupDataSources.get(this.primary);
            if (groupDataSource != null) {
                return groupDataSource.determineDataSource();
            } else {
                throw new CannotFindDataSourceException("dynamic-datasource can not find primary datasource");
            }
        }
    }

    public Map<String, DataSource> getDataSources() {
        return this.dataSourceMap;
    }

    public Map<String, GroupDataSource> getGroupDataSources() {
        return this.groupDataSources;
    }

    public DataSource getDataSource(String ds) {
        if (DsStrUtils.isEmpty(ds)) {
            return this.determinePrimaryDataSource();
        } else if (!this.groupDataSources.isEmpty() && this.groupDataSources.containsKey(ds)) {
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return ((GroupDataSource)this.groupDataSources.get(ds)).determineDataSource();
        } else if (this.dataSourceMap.containsKey(ds)) {
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return (DataSource)this.dataSourceMap.get(ds);
        } else if (this.strict) {
            throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named " + ds);
        } else {
            return this.determinePrimaryDataSource();
        }
    }

    public synchronized void addDataSource(String ds, DataSource dataSource) {
        DataSource oldDataSource = (DataSource)this.dataSourceMap.put(ds, dataSource);
        this.addGroupDataSource(ds, dataSource);
        if (oldDataSource != null) {
            this.closeDataSource(ds, oldDataSource, this.graceDestroy);
        }

        log.info("dynamic-datasource - add a datasource named [{}] success", ds);
    }

    private void addGroupDataSource(String ds, DataSource dataSource) {
        if (ds.contains("_")) {
            String group = ds.split("_")[0];
            GroupDataSource groupDataSource = (GroupDataSource)this.groupDataSources.get(group);
            if (groupDataSource == null) {
                try {
                    groupDataSource = new GroupDataSource(group, (DynamicDataSourceStrategy)this.strategy.getDeclaredConstructor().newInstance());
                    this.groupDataSources.put(group, groupDataSource);
                } catch (Exception e) {
                    throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
                }
            }

            groupDataSource.addDatasource(ds, dataSource);
        }

    }

    public synchronized void removeDataSource(String ds) {
        if (!DsStrUtils.hasText(ds)) {
            throw new RuntimeException("remove parameter could not be empty");
        } else if (this.primary.equals(ds)) {
            throw new RuntimeException("could not remove primary datasource");
        } else {
            if (this.dataSourceMap.containsKey(ds)) {
                DataSource dataSource = (DataSource)this.dataSourceMap.remove(ds);
                if (ds.contains("_")) {
                    String group = ds.split("_")[0];
                    if (this.groupDataSources.containsKey(group)) {
                        DataSource oldDataSource = ((GroupDataSource)this.groupDataSources.get(group)).removeDatasource(ds);
                        if (oldDataSource == null) {
                            log.warn("fail for remove datasource from group. dataSource: {} ,group: {}", ds, group);
                        }
                    }
                }

                this.closeDataSource(ds, dataSource, this.graceDestroy);
                log.info("dynamic-datasource - remove the database named [{}] success", ds);
            } else {
                log.warn("dynamic-datasource - could not find a database named [{}]", ds);
            }

        }
    }

    public void destroy() {
        log.info("dynamic-datasource start closing ....");

        for(Map.Entry<String, DataSource> item : this.dataSourceMap.entrySet()) {
            this.closeDataSource((String)item.getKey(), (DataSource)item.getValue(), false);
        }

        log.info("dynamic-datasource all closed success,bye");
    }

    public void afterPropertiesSet() {
        this.checkEnv();
        Map<String, DataSource> dataSources = new HashMap(16);

        for(DynamicDataSourceProvider provider : this.providers) {
            Map<String, DataSource> dsMap = provider.loadDataSources();
            if (dsMap != null) {
                dataSources.putAll(dsMap);
            }
        }

        for(Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
            this.addDataSource((String)dsItem.getKey(), (DataSource)dsItem.getValue());
        }

        if (this.groupDataSources.containsKey(this.primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), this.primary);
        } else if (this.dataSourceMap.containsKey(this.primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), this.primary);
        } else {
            log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size());
        }

    }

    private void checkEnv() {
        if (this.p6spy) {
            try {
                Class.forName("com.p6spy.engine.spy.P6DataSource");
                log.info("dynamic-datasource detect P6SPY plugin and enabled it");
            } catch (Exception e) {
                throw new RuntimeException("dynamic-datasource enabled P6SPY ,however without p6spy dependency", e);
            }
        }

        if (this.seata) {
            try {
                Class.forName("io.seata.rm.datasource.DataSourceProxy");
                log.info("dynamic-datasource detect ALIBABA SEATA and enabled it");
            } catch (Exception e) {
                throw new RuntimeException("dynamic-datasource enabled ALIBABA SEATA,however without seata dependency", e);
            }
        }

    }

    private void closeDataSource(String ds, DataSource dataSource, boolean graceDestroy) {
        try {
            DataSource realDataSource = null;
            if (dataSource instanceof ItemDataSource) {
                realDataSource = ((ItemDataSource)dataSource).getRealDataSource();
            } else {
                if (this.seata && dataSource instanceof DataSourceProxy) {
                    DataSourceProxy dataSourceProxy = (DataSourceProxy)dataSource;
                    realDataSource = dataSourceProxy.getTargetDataSource();
                }

                if (this.p6spy && dataSource instanceof P6DataSource) {
                    Field realDataSourceField = P6DataSource.class.getDeclaredField("realDataSource");
                    realDataSourceField.setAccessible(true);
                    realDataSource = (DataSource)realDataSourceField.get(dataSource);
                }
            }

            if (null == realDataSource) {
                realDataSource = dataSource;
            }

            if (null != realDataSource) {
                DataSourceDestroyer destroyer = new DefaultDataSourceDestroyer();
                if (graceDestroy) {
                    destroyer.asyncDestroy(ds, realDataSource);
                } else {
                    destroyer.destroy(ds, realDataSource);
                }
            }
        } catch (Exception e) {
            log.warn("dynamic-datasource closed datasource named [{}] failed", ds, e);
        }

    }

    public void setStrategy(final Class<? extends DynamicDataSourceStrategy> strategy) {
        this.strategy = strategy;
    }

    public void setPrimary(final String primary) {
        this.primary = primary;
    }

    public void setStrict(final Boolean strict) {
        this.strict = strict;
    }

    public void setP6spy(final Boolean p6spy) {
        this.p6spy = p6spy;
    }

    public void setSeata(final Boolean seata) {
        this.seata = seata;
    }

    public void setGraceDestroy(final Boolean graceDestroy) {
        this.graceDestroy = graceDestroy;
    }
}
  • DataSourceProperty (数据源配置参数)
java 复制代码
package com.baomidou.dynamic.datasource.creator;

import com.baomidou.dynamic.datasource.creator.atomikos.AtomikosConfig;
import com.baomidou.dynamic.datasource.creator.beecp.BeeCpConfig;
import com.baomidou.dynamic.datasource.creator.dbcp.Dbcp2Config;
import com.baomidou.dynamic.datasource.creator.druid.DruidConfig;
import com.baomidou.dynamic.datasource.creator.hikaricp.HikariCpConfig;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DataSourceProperty {
    private static final Logger log = LoggerFactory.getLogger(DataSourceProperty.class);
    private String poolName;
    private Class<? extends DataSource> type;
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private String jndiName;
    private Boolean seata = true;
    private Boolean p6spy = true;
    private Boolean lazy;
    private DatasourceInitProperties init = new DatasourceInitProperties();
    private DruidConfig druid = new DruidConfig();
    private HikariCpConfig hikari = new HikariCpConfig();
    private BeeCpConfig beecp = new BeeCpConfig();
    private Dbcp2Config dbcp2 = new Dbcp2Config();
    private AtomikosConfig atomikos = new AtomikosConfig();
    private String publicKey;

    public String getPoolName() {
        return this.poolName;
    }

    public Class<? extends DataSource> getType() {
        return this.type;
    }

    public String getDriverClassName() {
        return this.driverClassName;
    }

    public String getUrl() {
        return this.url;
    }

    public String getUsername() {
        return this.username;
    }

    public String getPassword() {
        return this.password;
    }

    public String getJndiName() {
        return this.jndiName;
    }

    public Boolean getSeata() {
        return this.seata;
    }

    public Boolean getP6spy() {
        return this.p6spy;
    }

    public Boolean getLazy() {
        return this.lazy;
    }

    public DatasourceInitProperties getInit() {
        return this.init;
    }

    public DruidConfig getDruid() {
        return this.druid;
    }

    public HikariCpConfig getHikari() {
        return this.hikari;
    }

    public BeeCpConfig getBeecp() {
        return this.beecp;
    }

    public Dbcp2Config getDbcp2() {
        return this.dbcp2;
    }

    public AtomikosConfig getAtomikos() {
        return this.atomikos;
    }

    public String getPublicKey() {
        return this.publicKey;
    }

    public void setPoolName(final String poolName) {
        this.poolName = poolName;
    }

    public void setType(final Class<? extends DataSource> type) {
        this.type = type;
    }

    public void setDriverClassName(final String driverClassName) {
        this.driverClassName = driverClassName;
    }

    public void setUrl(final String url) {
        this.url = url;
    }

    public void setUsername(final String username) {
        this.username = username;
    }

    public void setPassword(final String password) {
        this.password = password;
    }

    public void setJndiName(final String jndiName) {
        this.jndiName = jndiName;
    }

    public void setSeata(final Boolean seata) {
        this.seata = seata;
    }

    public void setP6spy(final Boolean p6spy) {
        this.p6spy = p6spy;
    }

    public void setLazy(final Boolean lazy) {
        this.lazy = lazy;
    }

    public void setInit(final DatasourceInitProperties init) {
        this.init = init;
    }

    public void setDruid(final DruidConfig druid) {
        this.druid = druid;
    }

    public void setHikari(final HikariCpConfig hikari) {
        this.hikari = hikari;
    }

    public void setBeecp(final BeeCpConfig beecp) {
        this.beecp = beecp;
    }

    public void setDbcp2(final Dbcp2Config dbcp2) {
        this.dbcp2 = dbcp2;
    }

    public void setAtomikos(final AtomikosConfig atomikos) {
        this.atomikos = atomikos;
    }

    public void setPublicKey(final String publicKey) {
        this.publicKey = publicKey;
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof DataSourceProperty)) {
            return false;
        } else {
            DataSourceProperty other = (DataSourceProperty)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                Object this$seata = this.getSeata();
                Object other$seata = other.getSeata();
                if (this$seata == null) {
                    if (other$seata != null) {
                        return false;
                    }
                } else if (!this$seata.equals(other$seata)) {
                    return false;
                }

                Object this$p6spy = this.getP6spy();
                Object other$p6spy = other.getP6spy();
                if (this$p6spy == null) {
                    if (other$p6spy != null) {
                        return false;
                    }
                } else if (!this$p6spy.equals(other$p6spy)) {
                    return false;
                }

                Object this$lazy = this.getLazy();
                Object other$lazy = other.getLazy();
                if (this$lazy == null) {
                    if (other$lazy != null) {
                        return false;
                    }
                } else if (!this$lazy.equals(other$lazy)) {
                    return false;
                }

                Object this$poolName = this.getPoolName();
                Object other$poolName = other.getPoolName();
                if (this$poolName == null) {
                    if (other$poolName != null) {
                        return false;
                    }
                } else if (!this$poolName.equals(other$poolName)) {
                    return false;
                }

                Object this$type = this.getType();
                Object other$type = other.getType();
                if (this$type == null) {
                    if (other$type != null) {
                        return false;
                    }
                } else if (!this$type.equals(other$type)) {
                    return false;
                }

                Object this$driverClassName = this.getDriverClassName();
                Object other$driverClassName = other.getDriverClassName();
                if (this$driverClassName == null) {
                    if (other$driverClassName != null) {
                        return false;
                    }
                } else if (!this$driverClassName.equals(other$driverClassName)) {
                    return false;
                }

                Object this$url = this.getUrl();
                Object other$url = other.getUrl();
                if (this$url == null) {
                    if (other$url != null) {
                        return false;
                    }
                } else if (!this$url.equals(other$url)) {
                    return false;
                }

                Object this$username = this.getUsername();
                Object other$username = other.getUsername();
                if (this$username == null) {
                    if (other$username != null) {
                        return false;
                    }
                } else if (!this$username.equals(other$username)) {
                    return false;
                }

                Object this$password = this.getPassword();
                Object other$password = other.getPassword();
                if (this$password == null) {
                    if (other$password != null) {
                        return false;
                    }
                } else if (!this$password.equals(other$password)) {
                    return false;
                }

                Object this$jndiName = this.getJndiName();
                Object other$jndiName = other.getJndiName();
                if (this$jndiName == null) {
                    if (other$jndiName != null) {
                        return false;
                    }
                } else if (!this$jndiName.equals(other$jndiName)) {
                    return false;
                }

                Object this$init = this.getInit();
                Object other$init = other.getInit();
                if (this$init == null) {
                    if (other$init != null) {
                        return false;
                    }
                } else if (!this$init.equals(other$init)) {
                    return false;
                }

                Object this$druid = this.getDruid();
                Object other$druid = other.getDruid();
                if (this$druid == null) {
                    if (other$druid != null) {
                        return false;
                    }
                } else if (!this$druid.equals(other$druid)) {
                    return false;
                }

                Object this$hikari = this.getHikari();
                Object other$hikari = other.getHikari();
                if (this$hikari == null) {
                    if (other$hikari != null) {
                        return false;
                    }
                } else if (!this$hikari.equals(other$hikari)) {
                    return false;
                }

                Object this$beecp = this.getBeecp();
                Object other$beecp = other.getBeecp();
                if (this$beecp == null) {
                    if (other$beecp != null) {
                        return false;
                    }
                } else if (!this$beecp.equals(other$beecp)) {
                    return false;
                }

                Object this$dbcp2 = this.getDbcp2();
                Object other$dbcp2 = other.getDbcp2();
                if (this$dbcp2 == null) {
                    if (other$dbcp2 != null) {
                        return false;
                    }
                } else if (!this$dbcp2.equals(other$dbcp2)) {
                    return false;
                }

                Object this$atomikos = this.getAtomikos();
                Object other$atomikos = other.getAtomikos();
                if (this$atomikos == null) {
                    if (other$atomikos != null) {
                        return false;
                    }
                } else if (!this$atomikos.equals(other$atomikos)) {
                    return false;
                }

                Object this$publicKey = this.getPublicKey();
                Object other$publicKey = other.getPublicKey();
                if (this$publicKey == null) {
                    if (other$publicKey != null) {
                        return false;
                    }
                } else if (!this$publicKey.equals(other$publicKey)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof DataSourceProperty;
    }

    public int hashCode() {
        int PRIME = 59;
        int result = 1;
        Object $seata = this.getSeata();
        result = result * 59 + ($seata == null ? 43 : $seata.hashCode());
        Object $p6spy = this.getP6spy();
        result = result * 59 + ($p6spy == null ? 43 : $p6spy.hashCode());
        Object $lazy = this.getLazy();
        result = result * 59 + ($lazy == null ? 43 : $lazy.hashCode());
        Object $poolName = this.getPoolName();
        result = result * 59 + ($poolName == null ? 43 : $poolName.hashCode());
        Object $type = this.getType();
        result = result * 59 + ($type == null ? 43 : $type.hashCode());
        Object $driverClassName = this.getDriverClassName();
        result = result * 59 + ($driverClassName == null ? 43 : $driverClassName.hashCode());
        Object $url = this.getUrl();
        result = result * 59 + ($url == null ? 43 : $url.hashCode());
        Object $username = this.getUsername();
        result = result * 59 + ($username == null ? 43 : $username.hashCode());
        Object $password = this.getPassword();
        result = result * 59 + ($password == null ? 43 : $password.hashCode());
        Object $jndiName = this.getJndiName();
        result = result * 59 + ($jndiName == null ? 43 : $jndiName.hashCode());
        Object $init = this.getInit();
        result = result * 59 + ($init == null ? 43 : $init.hashCode());
        Object $druid = this.getDruid();
        result = result * 59 + ($druid == null ? 43 : $druid.hashCode());
        Object $hikari = this.getHikari();
        result = result * 59 + ($hikari == null ? 43 : $hikari.hashCode());
        Object $beecp = this.getBeecp();
        result = result * 59 + ($beecp == null ? 43 : $beecp.hashCode());
        Object $dbcp2 = this.getDbcp2();
        result = result * 59 + ($dbcp2 == null ? 43 : $dbcp2.hashCode());
        Object $atomikos = this.getAtomikos();
        result = result * 59 + ($atomikos == null ? 43 : $atomikos.hashCode());
        Object $publicKey = this.getPublicKey();
        result = result * 59 + ($publicKey == null ? 43 : $publicKey.hashCode());
        return result;
    }

    public String toString() {
        return "DataSourceProperty(poolName=" + this.getPoolName() + ", type=" + this.getType() + ", driverClassName=" + this.getDriverClassName() + ", url=" + this.getUrl() + ", username=" + this.getUsername() + ", password=" + this.getPassword() + ", jndiName=" + this.getJndiName() + ", seata=" + this.getSeata() + ", p6spy=" + this.getP6spy() + ", lazy=" + this.getLazy() + ", init=" + this.getInit() + ", druid=" + this.getDruid() + ", hikari=" + this.getHikari() + ", beecp=" + this.getBeecp() + ", dbcp2=" + this.getDbcp2() + ", atomikos=" + this.getAtomikos() + ", publicKey=" + this.getPublicKey() + ")";
    }
}
  • DynamicDataSourceContextHolder (数据源上下文)
java 复制代码
package com.baomidou.dynamic.datasource.toolkit;

import java.util.ArrayDeque;
import java.util.Deque;
import org.springframework.core.NamedThreadLocal;

public final class DynamicDataSourceContextHolder {
    private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
        protected Deque<String> initialValue() {
            return new ArrayDeque();
        }
    };

    private DynamicDataSourceContextHolder() {
    }

    public static String peek() {
        return (String)((Deque)LOOKUP_KEY_HOLDER.get()).peek();
    }

    public static String push(String ds) {
        String dataSourceStr = DsStrUtils.isEmpty(ds) ? "" : ds;
        ((Deque)LOOKUP_KEY_HOLDER.get()).push(dataSourceStr);
        return dataSourceStr;
    }

    public static void poll() {
        Deque<String> deque = (Deque)LOOKUP_KEY_HOLDER.get();
        deque.poll();
        if (deque.isEmpty()) {
            LOOKUP_KEY_HOLDER.remove();
        }

    }

    public static void clear() {
        LOOKUP_KEY_HOLDER.remove();
    }
}
  • LoadBalanceDynamicDataSourceStrategy (数据源组选择策略-轮询)
java 复制代码
package com.baomidou.dynamic.datasource.strategy;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class LoadBalanceDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
    private final AtomicInteger index = new AtomicInteger(0);

    public String determineKey(List<String> dsNames) {
        return (String)dsNames.get(Math.abs(this.index.getAndAdd(1) % dsNames.size()));
    }
}
  • RandomDynamicDataSourceStrategy (数据源组选择策略-随机)
java 复制代码
package com.baomidou.dynamic.datasource.strategy;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class RandomDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
    public String determineKey(List<String> dsNames) {
        return (String)dsNames.get(ThreadLocalRandom.current().nextInt(dsNames.size()));
    }
}
  • DynamicDataSourceAopConfiguration
java 复制代码
package com.baomidou.dynamic.datasource.spring.boot.autoconfigure;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationAdvisor;
import com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationInterceptor;
import com.baomidou.dynamic.datasource.aop.DynamicLocalTransactionInterceptor;
import com.baomidou.dynamic.datasource.processor.DsHeaderProcessor;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import com.baomidou.dynamic.datasource.processor.DsSessionProcessor;
import com.baomidou.dynamic.datasource.processor.DsSpelExpressionProcessor;
import org.springframework.aop.Advisor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.context.expression.BeanFactoryResolver;

@Role(2)
@Configuration(
    proxyBeanMethods = false
)
public class DynamicDataSourceAopConfiguration {
    private final DynamicDataSourceProperties properties;

    public DynamicDataSourceAopConfiguration(DynamicDataSourceProperties properties) {
        this.properties = properties;
    }

    @Role(2)
    @Bean
    public static DynamicDataSourceProperties dynamicDataSourceProperties() {
        return new DynamicDataSourceProperties();
    }

    @Role(2)
    @Bean
    @ConditionalOnMissingBean
    public DsProcessor dsProcessor(BeanFactory beanFactory) {
        DsProcessor headerProcessor = new DsHeaderProcessor();
        DsProcessor sessionProcessor = new DsSessionProcessor();
        DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
        spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory));
        headerProcessor.setNextProcessor(sessionProcessor);
        sessionProcessor.setNextProcessor(spelExpressionProcessor);
        return headerProcessor;
    }

    @Role(2)
    @Bean
    @ConditionalOnProperty(
        prefix = "spring.datasource.dynamic.aop",
        name = {"enabled"},
        havingValue = "true",
        matchIfMissing = true
    )
    public Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
        DynamicDatasourceAopProperties aopProperties = this.properties.getAop();
        DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(aopProperties.getAllowedPublicOnly(), dsProcessor);
        DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor, DS.class);
        advisor.setOrder(aopProperties.getOrder());
        return advisor;
    }

    @Role(2)
    @Bean
    @ConditionalOnProperty(
        prefix = "spring.datasource.dynamic",
        name = {"seata"},
        havingValue = "false",
        matchIfMissing = true
    )
    public Advisor dynamicTransactionAdvisor() {
        DynamicDatasourceAopProperties aopProperties = this.properties.getAop();
        DynamicLocalTransactionInterceptor interceptor = new DynamicLocalTransactionInterceptor(aopProperties.getAllowedPublicOnly());
        return new DynamicDataSourceAnnotationAdvisor(interceptor, DSTransactional.class);
    }
}
  • DynamicDataSourceAutoConfiguration
java 复制代码
package com.baomidou.dynamic.datasource.spring.boot.autoconfigure;

import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import java.util.List;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.util.CollectionUtils;

@Configuration
@AutoConfigureBefore(
    value = {DataSourceAutoConfiguration.class},
    name = {"com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure"}
)
@Import({DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class, DynamicDataSourceAopConfiguration.class, DynamicDataSourceAssistConfiguration.class})
@ConditionalOnProperty(
    prefix = "spring.datasource.dynamic",
    name = {"enabled"},
    havingValue = "true",
    matchIfMissing = true
)
public class DynamicDataSourceAutoConfiguration implements InitializingBean {
    private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceAutoConfiguration.class);
    private final DynamicDataSourceProperties properties;
    private final List<DynamicDataSourcePropertiesCustomizer> dataSourcePropertiesCustomizers;

    public DynamicDataSourceAutoConfiguration(DynamicDataSourceProperties properties, ObjectProvider<List<DynamicDataSourcePropertiesCustomizer>> dataSourcePropertiesCustomizers) {
        this.properties = properties;
        this.dataSourcePropertiesCustomizers = (List)dataSourcePropertiesCustomizers.getIfAvailable();
    }

    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource(List<DynamicDataSourceProvider> providers) {
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(providers);
        dataSource.setPrimary(this.properties.getPrimary());
        dataSource.setStrict(this.properties.getStrict());
        dataSource.setStrategy(this.properties.getStrategy());
        dataSource.setP6spy(this.properties.getP6spy());
        dataSource.setSeata(this.properties.getSeata());
        dataSource.setGraceDestroy(this.properties.getGraceDestroy());
        return dataSource;
    }

    public void afterPropertiesSet() {
        if (!CollectionUtils.isEmpty(this.dataSourcePropertiesCustomizers)) {
            for(DynamicDataSourcePropertiesCustomizer customizer : this.dataSourcePropertiesCustomizers) {
                customizer.customize(this.properties);
            }
        }

    }
}
  • DefaultDataSourceCreator
java 复制代码
package com.baomidou.dynamic.datasource.creator;

import com.baomidou.dynamic.datasource.ds.ItemDataSource;
import com.baomidou.dynamic.datasource.enums.SeataMode;
import com.baomidou.dynamic.datasource.event.DataSourceInitEvent;
import com.baomidou.dynamic.datasource.support.ScriptRunner;
import com.baomidou.dynamic.datasource.toolkit.DsStrUtils;
import com.p6spy.engine.spy.P6DataSource;
import io.seata.rm.datasource.DataSourceProxy;
import io.seata.rm.datasource.xa.DataSourceProxyXA;
import java.util.List;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DefaultDataSourceCreator {
    private static final Logger log = LoggerFactory.getLogger(DefaultDataSourceCreator.class);
    private List<DataSourceCreator> creators;
    private Boolean lazy = false;
    private Boolean p6spy = false;
    private Boolean seata = false;
    private SeataMode seataMode;
    private String publicKey;
    private DataSourceInitEvent dataSourceInitEvent;

    public DefaultDataSourceCreator() {
        this.seataMode = SeataMode.AT;
        this.publicKey = "MFwwDQ**********AwEAAQ==";
    }

    public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
        DataSourceCreator dataSourceCreator = null;

        for(DataSourceCreator creator : this.creators) {
            if (creator.support(dataSourceProperty)) {
                dataSourceCreator = creator;
                break;
            }
        }

        if (dataSourceCreator == null) {
            throw new IllegalStateException("creator must not be null,please check the DataSourceCreator");
        } else {
            String propertyPublicKey = dataSourceProperty.getPublicKey();
            if (DsStrUtils.isEmpty(propertyPublicKey)) {
                dataSourceProperty.setPublicKey(this.publicKey);
            }

            Boolean propertyLazy = dataSourceProperty.getLazy();
            if (propertyLazy == null) {
                dataSourceProperty.setLazy(this.lazy);
            }

            if (this.dataSourceInitEvent != null) {
                this.dataSourceInitEvent.beforeCreate(dataSourceProperty);
            }

            DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
            if (this.dataSourceInitEvent != null) {
                this.dataSourceInitEvent.afterCreate(dataSource);
            }

            this.runScrip(dataSource, dataSourceProperty);
            return this.wrapDataSource(dataSource, dataSourceProperty);
        }
    }

    private void runScrip(DataSource dataSource, DataSourceProperty dataSourceProperty) {
        DatasourceInitProperties initProperty = dataSourceProperty.getInit();
        String schema = initProperty.getSchema();
        String data = initProperty.getData();
        if (DsStrUtils.hasText(schema) || DsStrUtils.hasText(data)) {
            ScriptRunner scriptRunner = new ScriptRunner(initProperty.isContinueOnError(), initProperty.getSeparator());
            if (DsStrUtils.hasText(schema)) {
                scriptRunner.runScript(dataSource, schema);
            }

            if (DsStrUtils.hasText(data)) {
                scriptRunner.runScript(dataSource, data);
            }
        }

    }

    private DataSource wrapDataSource(DataSource dataSource, DataSourceProperty dataSourceProperty) {
        String name = dataSourceProperty.getPoolName();
        DataSource targetDataSource = dataSource;
        Boolean enabledP6spy = this.p6spy && dataSourceProperty.getP6spy();
        if (enabledP6spy) {
            targetDataSource = new P6DataSource(dataSource);
            log.debug("dynamic-datasource [{}] wrap p6spy plugin", name);
        }

        Boolean enabledSeata = this.seata && dataSourceProperty.getSeata();
        if (enabledSeata) {
            if (SeataMode.XA == this.seataMode) {
                targetDataSource = new DataSourceProxyXA(targetDataSource);
            } else {
                targetDataSource = new DataSourceProxy(targetDataSource);
            }

            log.debug("dynamic-datasource [{}] wrap seata plugin transaction mode ", name);
        }

        return new ItemDataSource(name, dataSource, targetDataSource, enabledP6spy, enabledSeata, this.seataMode);
    }

    public void setCreators(final List<DataSourceCreator> creators) {
        this.creators = creators;
    }

    public void setLazy(final Boolean lazy) {
        this.lazy = lazy;
    }

    public void setP6spy(final Boolean p6spy) {
        this.p6spy = p6spy;
    }

    public void setSeata(final Boolean seata) {
        this.seata = seata;
    }

    public void setSeataMode(final SeataMode seataMode) {
        this.seataMode = seataMode;
    }

    public void setPublicKey(final String publicKey) {
        this.publicKey = publicKey;
    }

    public void setDataSourceInitEvent(final DataSourceInitEvent dataSourceInitEvent) {
        this.dataSourceInitEvent = dataSourceInitEvent;
    }
}
十八、多数据源的事务管理

待续...

相关推荐
漫漫求2 小时前
Java内存模型【JMM】、JVM内存模型
java·开发语言·jvm
原来是好奇心2 小时前
深入Spring Boot源码(五):外部化配置与Profile机制深度解析
java·源码·springboot
IT界的奇葩2 小时前
OAuth2 单点登录流程图
java·流程图·oauth2·单点登录·sso
ZHang......3 小时前
LeetCode 1114. 按序打印
java·开发语言·算法
程序员欣宸3 小时前
LangChain4j实战之四:集成到spring-boot
java·人工智能·spring boot
慧都小项3 小时前
Parasoft Jtest 如何用 JSON 文件驱动Java 测试自动化
java·自动化·json
Hui Baby3 小时前
全局事务入口感知子事务方法-TCC
java·开发语言·数据库
爱笑的眼睛113 小时前
FastAPI 请求验证:超越 Pydantic 基础,构建企业级验证体系
java·人工智能·python·ai
czlczl200209254 小时前
Spring Boot 参数校验进阶:抛弃复杂的 Group 分组,用 @AssertTrue 实现“动态逻辑校验”
java·spring boot·后端