Mybatis SpringBoot 动态数据源

443 阅读3分钟

大家好,我是想想。

最近在研读 SpringBoot 源码,再结合之前 Spring 源码部分,真令我茅塞顿开。他们之间如何依赖,如何配置。在没读源码之前,我只知道怎么用,读完之后,才知道它为什么会这样用!

之前画了一张 SpringBoot 自动配置实现过程的源码过程图,因为放不了大图片,我再如何压缩也放不...

这里先放一张缩略图

适合使用SpringBoot 1年以上的朋友们看。想要原图的话,可以后台留言我。

正文开始

Spring 内置一个 AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,然后根据不同的 key,返回不同的数据源,因为 AbstractRoutingDataSource 也是一个 DataSource 接口 ,因此,应用程序可以先设置好 key,访问数据库的代码就可以从 AbstractRoutingDataSource 拿到对应的一个真实数据源,从而访问指定数据库。

这是Spring提供的类

这是Spring提供的类

依赖关系

依赖关系

分析成员变量

 @Nullable
 // 存放多个数据源的 map 
 private Map<ObjectObject> targetDataSources;

 @Nullable
 // 默认数据源
 private Object defaultTargetDataSource;

 private boolean lenientFallback = true;

 private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

 @Nullable
 // 上面的 targetDataSources 也会在这里存一份
 // 区别就是 resolvedDataSources 是数据源解析后才会放到这个map里
 private Map<ObjectDataSource> resolvedDataSources;

 @Nullable
 private DataSource resolvedDefaultDataSource;

案例

核心依赖

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <!-- oracle驱动 -->
        <dependency>
            <groupId>oracle.jdbc</groupId>
            <artifactId>ojdbc6</artifactId>
            <version>11.2.0.4</version>
        </dependency>
        
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

因为 mybatis 和 oracle 并没有集成进 SpringBoot Starter 中,所以需要指定版本

这里忽略了 druid、springboot 等依赖。

spring:
  druid:
    datasource:
      master:
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://cloud:13388/test?useSSL=false&serverTimezone=UTC
        username: root
        password: root
      slave:
        driver-class-name: oracle.jdbc.OracleDriver
        jdbc-url: jdbc:oracle:thin:@cloud:11521:xe
        username: system
        password: oracle

编写两个数据源,同时存在 dept 表,创建 mapper、service、controller

数据源读取配置

创建 MyDataSourceConfiguratioin 用于读取我们在 yaml 中配置的多数据源

@Configuration
public class MyDataSourceConfiguratioin {

    @Bean("masterDataSource")
    @ConfigurationProperties(prefix = "spring.druid.datasource.master")
    DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean("slaveDataSource")
    @ConfigurationProperties(prefix = "spring.druid.datasource.slave")
    DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @Primary
   // 在Spring中,@Primary是一个注解,用于标记一个Bean作为优先选择的主要Bean。
    // 当多个相同类型的Bean被注册时,Spring框架会使用@Primary注解标记
    // 的那个Bean作为默认的Bean。这样就可以避免出现依赖注入歧义的问题
    DataSource primaryDataSource(
      @Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
      @Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource
    ) {
        Map<ObjectObjectmap = new HashMap<>();
        map.put("masterDataSource", masterDataSource);
        map.put("slaveDataSource", slaveDataSource);
        RoutingDataSource routing = new RoutingDataSource();
        routing.setTargetDataSources(map);
       // 前面我们讲了 defaultTargetDataSource 是默认数据源
       // targetDataSources 则是存放多个数据源的 map
        routing.setDefaultTargetDataSource(masterDataSource);
        return routing;
    }
}

创建 RoutingDataSource 实现 AbstractRoutingDataSource 来达到实现动态数据源功能

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return RoutingDataSourceContext.getDataSourceRoutingKey();
    }
}

我们可以覆盖 determineCurrentLookupKey() 功能实现数据隔离。通过这个方法通过不同的key获取到不同的数据源。

public class RoutingDataSourceContext  {

    // holds data source key in thread local:
    static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();

   // 如果没有,则默认使用 master 数据源,这样容错性高一点
    public static String getDataSourceRoutingKey() {
        String key = threadLocalDataSourceKey.get();
        return key == null ? "masterDataSource" : key;
    }

    public RoutingDataSourceContext(String key) {
        threadLocalDataSourceKey.set(key);
    }

    public void close() {
        threadLocalDataSourceKey.remove();
    }
}

这样我们就实现了数据源动态切换

当然还有最重要的一步!

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@MapperScan("com.liuyuncen.mapper")
public class RoutingDatasourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(RoutingDatasourceApplication.class,args);
    }
}

一定要排除 DataSourceAutoConfiguration 这个类,因为他会去获取默认的 DataSource 配置,因为我们已经改写了 yaml 配置(在里面添加了 master、slave 等,默认的 DataSource 不认识)

测试Controller

@GetMapping("/findAllProductM")
public String findAllProductM() {
  new RoutingDataSourceContext("masterDataSource");
  deptService.findAllProductM();
  return "success";
}

通过提前存储在 RoutingDataSource 中map,通过 key 获取到对应的数据源。这样mapper 接口中就会采用指定的数据源。

当然,如果不想通过 new RoutingDataSourceContext() 去获取,我们也可以优化成注解

优化-采用注解方式

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoutingWith {

    String value() default "master";
}

创建 RoutingWith 注解,默认使用 "master" 方式

使用动态代理,全局扫描 routingWith,在执行此方法前,通过 new RoutingDataSourceContext() 方式提前获取对应数据源

@Aspect
@Component
public class RoutingAspect {

  @Around("@annotation(routingWith)")
  public Object routingWithDataSource(ProceedingJoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
    String key = routingWith.value();
    RoutingDataSourceContext ctx = new RoutingDataSourceContext(key);
    return joinPoint.proceed();
  }
}

测试 Controller

@RoutingWith("slaveDataSource")
@GetMapping("/findAllProductS")
public String findAllProductS() {
  deptService.findAllProductS();
  return "success";
}

给一张测试案例吧

控制台打印案例

控制台打印案例

下面附上未展示的代码和代码结构

实体层

实体层

dao层

dao层

service层

service层