
【SpringBoot DB 系列】Mybatis 基于 AbstractRoutingDataSource 與 AOP 實現(xiàn)多數(shù)據(jù)源切換
前面一篇博文介紹了 Mybatis 多數(shù)據(jù)源的配置,簡單來講就是一個數(shù)據(jù)源一個配置指定,不同數(shù)據(jù)源的 Mapper 分開指定;本文將介紹另外一種方式,借助AbstractRoutingDataSource來實現(xiàn)動態(tài)切換數(shù)據(jù)源,并通過自定義注解方式 + AOP 來實現(xiàn)數(shù)據(jù)源的指定
I. 環(huán)境準(zhǔn)備
1. 數(shù)據(jù)庫相關(guān)
以 mysql 為例進行演示說明,因為需要多數(shù)據(jù)源,一個最簡單的 case 就是一個物理庫上多個邏輯庫,本文是基于本機的 mysql 進行操作
創(chuàng)建數(shù)據(jù)庫test 與 story,兩個庫下都存在一個表money (同名同結(jié)構(gòu)表,但是數(shù)據(jù)不同哦)
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用戶名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '錢',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 項目環(huán)境
本項目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA進行開發(fā)
下面是核心的pom.xml(源碼可以再文末獲取)
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
配置文件信息application.yml
# 數(shù)據(jù)庫相關(guān)配置,請注意這個配置和之前一篇博文的不一致,后面會給出原因
spring:
dynamic:
datasource:
story:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
test:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
# 日志相關(guān)
logging:
level:
root: info
org:
springframework:
jdbc:
core: debug
II. 多數(shù)據(jù)源配置
強烈建議沒有看上一篇博文的小伙伴,先看一下上篇博文 【DB 系列】Mybatis 多數(shù)據(jù)源配置與使用
在開始之前,先有必要回顧一下之前 Mybatis 多數(shù)據(jù)源配置的主要問題在哪里
- 多加一個數(shù)據(jù)源,需要多一份配置
- Mapper 文件需要分包處理,對開發(fā)人員而言這是個潛在的坑
針對上面這個,那我們想實現(xiàn)的目的也很清晰了,解決上面兩個問題
1. AbstractRoutingDataSource
實現(xiàn)多數(shù)據(jù)源的關(guān)鍵,從名字上就可以看出,它就是用來路由具體的數(shù)據(jù)源的,其核心代碼如
// 返回選中的數(shù)據(jù)源
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
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;
}
}
@Nullable
protected abstract Object determineCurrentLookupKey();
其中determineCurrentLookupKey需要我們自己來實現(xiàn),到底返回哪個數(shù)據(jù)源
2. 動態(tài)數(shù)據(jù)源實現(xiàn)
我們創(chuàng)建一個DynamicDataSource繼承自上面的抽象類
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String dataBaseType = DSTypeContainer.getDataBaseType();
return dataBaseType;
}
}
注意上面的實現(xiàn)方法,怎樣決定具體的返回數(shù)據(jù)源呢?
一個可考慮的方法是,在 Mapper 文件上添加一個注解@DS,里面指定對應(yīng)的數(shù)據(jù)源,然后再執(zhí)行時,通過它來確定具體需要執(zhí)行的數(shù)據(jù)源;
因為上面的實現(xiàn)沒有傳參,因此我們考慮借助線程上下文的方式來傳遞信息
public class DSTypeContainer {
private static final ThreadLocal<String> TYPE = new ThreadLocal<String>();
public static String defaultType;
/**
* 往當(dāng)前線程里設(shè)置數(shù)據(jù)源類型
*
* @param dataBase
*/
public static void setDataBaseType(String dataBase) {
if (StringUtils.isEmpty(dataBase)) {
dataBase = defaultType;
}
TYPE.set(dataBase);
System.err.println("[將當(dāng)前數(shù)據(jù)源改為]:" + dataBase);
}
/**
* 獲取數(shù)據(jù)源類型
*
* @return
*/
public static String getDataBaseType() {
String database = TYPE.get();
System.err.println("[獲取當(dāng)前數(shù)據(jù)源的類型為]:" + database);
return database;
}
/**
* 清空數(shù)據(jù)類型
*/
public static void clearDataBaseType() {
TYPE.remove();
}
}
3. 注解實現(xiàn)
上面雖然給出了數(shù)據(jù)源選擇的策略,從線程上下文中獲取DataBaseType,但是應(yīng)該怎樣向線程上下文中塞這個數(shù)據(jù)呢?
我們需要支持的方案必然是在 Sql 執(zhí)行之前,先攔截它,寫入這個DataBaseType,因此我們可以考慮在xxxMapper接口上,定義一個注解,然后攔截它的訪問執(zhí)行,在執(zhí)行之前獲取注解中指定的數(shù)據(jù)源寫入上下文,在執(zhí)行之后清楚上下文
一個最基礎(chǔ)的數(shù)據(jù)源注解@DS
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DS {
String value() default "";
}
注解攔截
@Aspect
@Component
public class DsAspect {
// 攔截類上有DS注解的方法調(diào)用
@Around("@within(DS)")
public Object dsAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
DS ds = (DS) proceedingJoinPoint.getSignature().getDeclaringType().getAnnotation(DS.class);
try {
// 寫入線程上下文,應(yīng)該用哪個DB
DSTypeContainer.setDataBaseType(ds == null ? null : ds.value());
return proceedingJoinPoint.proceed();
} finally {
// 清空上下文信息
DSTypeContainer.clearDataBaseType();
}
}
}
4. 注冊配置
接下來就是比較關(guān)鍵的數(shù)據(jù)源配置了,我們現(xiàn)在需要注冊DynamicDataSource,然后將他提供給SqlSessionFactory,在這里,我們希望解決即便多加數(shù)據(jù)源也不需要修改配置,所以我們調(diào)整了一下數(shù)據(jù)源的配置結(jié)構(gòu)
spring:
dynamic:
datasource:
story:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
test:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
然后給出一個加載上面配置的配置類DSProperties
@Data
@ConfigurationProperties(prefix = "spring.dynamic")
public class DSProperties {
private Map<String, DataSourceProperties> datasource;
}
然后我們的AutoConfiguration類的實現(xiàn)方式就相對明確了(建議對比上一篇博文中的配置類)
@Configuration
@EnableConfigurationProperties(DSProperties.class)
@MapperScan(basePackages = {"com.git.hui.boot.multi.datasource.mapper"},
sqlSessionFactoryRef = "SqlSessionFactory")
public class DynamicDataSourceConfig {
@SuppressWarnings("unchecked")
@Bean(name = "dynamicDataSource")
public DynamicDataSource DataSource(DSProperties dsProperties) {
Map targetDataSource = new HashMap<>(8);
dsProperties.getDatasource().forEach((k, v) -> {
targetDataSource.put(k, v.initializeDataSourceBuilder().build());
});
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSource);
// 設(shè)置默認(rèn)的數(shù)據(jù)庫,下面這個賦值方式寫法不太推薦,這里只是為了方便而已
DSTypeContainer.defaultType = (String) targetDataSource.keySet().stream().findFirst().get();
dataSource.setDefaultTargetDataSource(targetDataSource.get(DSTypeContainer.defaultType));
return dataSource;
}
@Bean(name = "SqlSessionFactory")
public SqlSessionFactory test1SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
bean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*/*.xml"));
return bean.getObject();
}
}
5. 數(shù)據(jù)庫實體類
項目結(jié)構(gòu)圖

所有前面的東西屬于通用配置相關(guān),接下來給出具體的數(shù)據(jù)庫操作相關(guān)實體類、Mapper 類
數(shù)據(jù)庫實體類StoryMoneyEntity
@Data
public class StoryMoneyEntity {
private Integer id;
private String name;
private Long money;
private Integer isDeleted;
private Timestamp createAt;
private Timestamp updateAt;
}
mapper 定義接口 StoryMoneyMapper + TestMoneyMapper
@DS(value = "story")
@Mapper
public interface StoryMoneyMapper {
List<StoryMoneyEntity> findByIds(List<Integer> ids);
}
@DS(value = "test")
@Mapper
public interface TestMoneyMapper {
List<TestMoneyEntity> findByIds(List<Integer> ids);
}
對應(yīng)的 xml 文件
<?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.git.hui.boot.multi.datasource.mapper.StoryMoneyMapper">
<resultMap id="BaseResultMap" type="com.git.hui.boot.multi.datasource.entity.StoryMoneyEntity">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="money" property="money" jdbcType="INTEGER"/>
<result column="is_deleted" property="isDeleted" jdbcType="TINYINT"/>
<result column="create_at" property="createAt" jdbcType="TIMESTAMP"/>
<result column="update_at" property="updateAt" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="money_po">
id, `name`, money, is_deleted, create_at, update_at
</sql>
<select id="findByIds" parameterType="list" resultMap="BaseResultMap">
select
<include refid="money_po"/>
from money where id in
<foreach item="id" collection="list" separator="," open="(" close=")" index="">
#{id}
</foreach>
</select>
</mapper>
<!-- 省略第二個xml文件 內(nèi)容基本一致-->
數(shù)據(jù)庫操作封裝類StoryMoneyRepository + TestMoneyRepository
@Repository
public class StoryMoneyRepository {
@Autowired
private StoryMoneyMapper storyMoneyMapper;
public void query() {
List<StoryMoneyEntity> list = storyMoneyMapper.findByIds(Arrays.asList(1, 1000));
System.out.println(list);
}
}
@Repository
public class TestMoneyRepository {
@Autowired
private TestMoneyMapper testMoneyMapper;
public void query() {
List<TestMoneyEntity> list = testMoneyMapper.findByIds(Arrays.asList(1, 1000));
System.out.println(list);
}
}
6. 測試
最后簡單的測試下,動態(tài)數(shù)據(jù)源切換是否生效
@SpringBootApplication
public class Application {
public Application(StoryMoneyRepository storyMoneyRepository, TestMoneyRepository testMoneyRepository) {
storyMoneyRepository.query();
testMoneyRepository.query();
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
輸出日志如下

6.小結(jié)
本文主要給出了一種基于AbstractRoutingDataSource + AOP實現(xiàn)動態(tài)數(shù)據(jù)源切換的實現(xiàn)方式,使用了下面三個知識點
-
AbstractRoutingDataSource實現(xiàn)動態(tài)數(shù)據(jù)源切換 - 自定義
@DS注解 + AOP 指定 Mapper 對應(yīng)的數(shù)據(jù)源 -
ConfigurationProperties方式支持添加數(shù)據(jù)源無需修改配置
II. 其他
0. 項目
相關(guān)博文
- 【DB 系列】Mybatis 多數(shù)據(jù)源配置與使用
- 【DB 系列】JdbcTemplate 之多數(shù)據(jù)源配置與使用
- 【DB 系列】Mybatis-Plus 代碼自動生成
- 【DB 系列】MybatisPlus 整合篇
- 【DB 系列】Mybatis+注解整合篇
- 【DB 系列】Mybatis+xml 整合篇
源碼
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 源碼: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/109-multi-datasource-mybatis
1. 一灰灰 Blog
盡信書則不如,以上內(nèi)容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發(fā)現(xiàn) bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人博客,記錄所有學(xué)習(xí)和工作中的博文,歡迎大家前去逛逛
- 一灰灰 Blog 個人博客 https://blog.hhui.top
- 一灰灰 Blog-Spring 專題博客 http://spring.hhui.top
- 微信公眾號: 一灰灰blog