Mybatis 动态切换数据源
当项目中用到多数据源的时候,常用到的一种做法是通过配置,将不同路径下的 Mapper 和不同的数据源关联起来。但是在某些场景下,这种方式可能不是最佳的。 例如:当涉及多个地区,每个地区会有各自独立的数据库,而这些数据库里的表结构都是一样的,只是数据不一样。我们可能要将相同的 SQL 在不同地区的数据库中执行。此时,如果为每个地区都分别建立一套一模一样的 Mapper 显然是不合适的,既麻烦,又不利于维护。
这里分享一种利用 Spring 的 AbstractRoutingDataSource
进行动态数据源切换的方法。
完整的代码参考:mybatis-dynamic-data-source-dome
核心思路
AbstractRoutingDataSource
是 spring-jdbc
中的一个抽象类。它提供了一种键( LookupKey
)和多数据源( TargetDataSources
)的映射。
在继承它的时候,需要我们实现 determineCurrentLookupKey()
方法。当要连接到数据源时,Spring 会通过此方法,获取相应的数据源链接。
据此,我们可以利用 AOP 在需要的地方做切面,通过设置 LookupKey
来动态切换数据源。
实现过程
- 引入需要的依赖
<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>
- 添加相关的配置
我这里配置了两个数据源,一个 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
- 实现
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();
}
}
- 配置数据源
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);
}
}
- 增加用于做切面的注解
这里我把 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();
}
- 新增一个配置类。和配置文件中的
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;
}
- 编写切面逻辑
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();
}
}
}
- 在需要动态切换数据源的地方打上
@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);
}
- 编写测试类验证
这里我们通过修改 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()));
}
}