Mybatis 动态切换数据源

原创
2019/11/21 18:14
阅读数 65

Mybatis 动态切换数据源

当项目中用到多数据源的时候,常用到的一种做法是通过配置,将不同路径下的 Mapper 和不同的数据源关联起来。但是在某些场景下,这种方式可能不是最佳的。 例如:当涉及多个地区,每个地区会有各自独立的数据库,而这些数据库里的表结构都是一样的,只是数据不一样。我们可能要将相同的 SQL 在不同地区的数据库中执行。此时,如果为每个地区都分别建立一套一模一样的 Mapper 显然是不合适的,既麻烦,又不利于维护。

这里分享一种利用 Spring 的 AbstractRoutingDataSource 进行动态数据源切换的方法。 完整的代码参考:mybatis-dynamic-data-source-dome

核心思路

AbstractRoutingDataSource 是 spring-jdbc 中的一个抽象类。它提供了一种键( LookupKey)和多数据源( TargetDataSources)的映射。 在继承它的时候,需要我们实现 determineCurrentLookupKey() 方法。当要连接到数据源时,Spring 会通过此方法,获取相应的数据源链接。 据此,我们可以利用 AOP 在需要的地方做切面,通过设置 LookupKey 来动态切换数据源。

实现过程

  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>

<!--Mybatis-->
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.1.1</version>
</dependency>

<!--Oracle-->
<dependency>
  <groupId>com.oracle.ojdbc</groupId>
  <artifactId>ojdbc8</artifactId>
  <scope>runtime</scope>
</dependency>
<!--MySql-->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <scope>runtime</scope>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
  </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>
  1. 添加相关的配置

我这里配置了两个数据源,一个 Oracle,一个 MySql。

spring:
  datasource:
    ds1:
      driver-class-name: oracle.jdbc.OracleDriver
      jdbc-url: jdbc:oracle:thin:@host:port:sid
      username: username
      password: password
    ds2:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://host:port/datasource
      username: username
      password: password

mybatis:
  type-aliases-package: dev.yorkecao.demo.mybatis.entity
  configuration:
    map-underscore-to-camel-case: true

demo:
  env: dev
  1. 实现 AbstractRoutingDataSource
package dev.yorkecao.demo.mybatis.config;

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

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

public class MultipleDataSource extends AbstractRoutingDataSource {

    private static final ThreadLocal<String> currentLookupKey = new InheritableThreadLocal<>();

    public MultipleDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return currentLookupKey.get();
    }

    public static void setCurrentLookupKey(String lookupKey) {
        currentLookupKey.set(lookupKey);
    }

    public static void clearLookupKey() {
        currentLookupKey.remove();
    }
}
  1. 配置数据源
package dev.yorkecao.demo.mybatis.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

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

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.ds1")
    public DataSource getDs1DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.ds2")
    public DataSource getDs2DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public MultipleDataSource getMultipleDataSource() {
        Map<Object, Object> dataSources = new HashMap<>();
        dataSources.put("ds1", getDs1DataSource());
        dataSources.put("ds2", getDs2DataSource());

        return new MultipleDataSource(getDs1DataSource(), dataSources);
    }
}
  1. 增加用于做切面的注解

这里我把 value 的类型设为字符串数组。字符串的格式为“${env}:${datasource}”,这样可以在切面中解析,根据当前的 env,动态设置相应的 datasource

package dev.yorkecao.demo.mybatis.annotation;

import java.lang.annotation.*;

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

    String[] value();
}
  1. 新增一个配置类。和配置文件中的 demo 相对应

我这里是通过配置文件设置 env,再据此解析出要使用的 datasource。大家可以根据自己的实际需求设计自定义逻辑。

package dev.yorkecao.demo.mybatis.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "demo")
public class DemoConfig {

    private String env;
}
  1. 编写切面逻辑
package dev.yorkecao.demo.mybatis.aop;

import dev.yorkecao.demo.mybatis.annotation.DynamicDataSource;
import dev.yorkecao.demo.mybatis.config.DemoConfig;
import dev.yorkecao.demo.mybatis.config.MultipleDataSource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Aspect
@Component
public class DynamicDataSourceAspect {

    @Autowired
    private DemoConfig demoConfig;

    @Pointcut("@annotation(dev.yorkecao.demo.mybatis.annotation.DynamicDataSource)")
    public void mapperMethod() {}

    @Around("mapperMethod()")
    public Object setDataSource(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        DynamicDataSource dynamicDataSource = method.getAnnotation(DynamicDataSource.class);
        String[] configArray = dynamicDataSource.value();
        Map<String, String> configMap = Arrays.stream(configArray)
                .map(t -> t.split(":"))
                .collect(Collectors.toMap(t -> t[0], t -> t[1]));
        String dataSourceKey = configMap.get(demoConfig.getEnv());
        log.debug("设置 dataSource:{}", dataSourceKey);
        MultipleDataSource.setCurrentLookupKey(dataSourceKey);

        try {
            return joinPoint.proceed();
        } finally {
            log.info("移除 dataSource:{}", dataSourceKey);
            MultipleDataSource.clearLookupKey();
        }
    }
}
  1. 在需要动态切换数据源的地方打上 @DynamicDataSource 注解
package dev.yorkecao.demo.mybatis.mapper;

import dev.yorkecao.demo.mybatis.annotation.DynamicDataSource;
import dev.yorkecao.demo.mybatis.entity.Demo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface DemoMapper {

    @DynamicDataSource({"dev:ds1", "test:ds2"})
    Demo selectDemo(@Param("id") String id);
}
  1. 编写测试类验证

这里我们通过修改 ENV 的值,观察其在不同的情况下是否访问了不同的数据源

package dev.yorkecao.demo.mybatis.mapper;

import dev.yorkecao.demo.mybatis.config.DemoConfig;
import dev.yorkecao.demo.mybatis.entity.Demo;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class DemoMapperTest {

    private static final String ENV = "test";

    @Autowired
    private DemoConfig demoConfig;
    @Autowired
    private DemoMapper demoMapper;


    @Before
    public void setEnv() {
        demoConfig.setEnv(ENV);
    }

    @Test
    public void selectDemo() {
        Demo demo = demoMapper.selectDemo("1");

        Assert.assertTrue(ENV.equals(demo.getName()));
    }
}
展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部