【SpringBoot DB 系列】Mybatis 基於 AbstractRoutingDataSource 與 AOP 實現多數據源切換
前面一篇博文介紹了 Mybatis 多數據源的配置,簡單來說就是一個數據源一個配置指定,不一樣數據源的 Mapper 分開指定;本文將介紹另一種方式,藉助AbstractRoutingDataSource
來實現動態切換數據源,並經過自定義註解方式 + AOP 來實現數據源的指定java
<!-- more -->mysql
以 mysql 爲例進行演示說明,由於須要多數據源,一個最簡單的 case 就是一個物理庫上多個邏輯庫,本文是基於本機的 mysql 進行操做git
建立數據庫test
與 story
,兩個庫下都存在一個表money
(同名同結構表,可是數據不一樣哦)github
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 '建立時間', `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;
本項目藉助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
進行開發spring
下面是核心的pom.xml
(源碼能夠再文末獲取)sql
<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
數據庫
# 數據庫相關配置,請注意這個配置和以前一篇博文的不一致,後面會給出緣由 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: # 日誌相關 logging: level: root: info org: springframework: jdbc: core: debug
強烈建議沒有看上一篇博文的小夥伴,先看一下上篇博文 【DB 系列】Mybatis 多數據源配置與使用
在開始以前,先有必要回顧一下以前 Mybatis 多數據源配置的主要問題在哪裏微信
針對上面這個,那咱們想實現的目的也很清晰了,解決上面兩個問題mybatis
實現多數據源的關鍵,從名字上就能夠看出,它就是用來路由具體的數據源的,其核心代碼如app
// 返回選中的數據源 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
須要咱們本身來實現,到底返回哪一個數據源
咱們建立一個DynamicDataSource
繼承自上面的抽象類
public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { String dataBaseType = DSTypeContainer.getDataBaseType(); return dataBaseType; } }
注意上面的實現方法,怎樣決定具體的返回數據源呢?
一個可考慮的方法是,在 Mapper 文件上添加一個註解@DS
,裏面指定對應的數據源,而後再執行時,經過它來肯定具體須要執行的數據源;
由於上面的實現沒有傳參,所以咱們考慮藉助線程上下文的方式來傳遞信息
public class DSTypeContainer { private static final ThreadLocal<String> TYPE = new ThreadLocal<String>(); public static String defaultType; /** * 往當前線程裏設置數據源類型 * * @param dataBase */ public static void setDataBaseType(String dataBase) { if (StringUtils.isEmpty(dataBase)) { dataBase = defaultType; } TYPE.set(dataBase); System.err.println("[將當前數據源改成]:" + dataBase); } /** * 獲取數據源類型 * * @return */ public static String getDataBaseType() { String database = TYPE.get(); System.err.println("[獲取當前數據源的類型爲]:" + database); return database; } /** * 清空數據類型 */ public static void clearDataBaseType() { TYPE.remove(); } }
上面雖然給出了數據源選擇的策略,從線程上下文中獲取DataBaseType
,可是應該怎樣向線程上下文中塞這個數據呢?
咱們須要支持的方案必然是在 Sql 執行以前,先攔截它,寫入這個DataBaseType
,所以咱們能夠考慮在xxxMapper
接口上,定義一個註解,而後攔截它的訪問執行,在執行以前獲取註解中指定的數據源寫入上下文,在執行以後清楚上下文
一個最基礎的數據源註解@DS
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface DS { String value() default ""; }
註解攔截
@Aspect @Component public class DsAspect { // 攔截類上有DS註解的方法調用 @Around("@within(DS)") public Object dsAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { DS ds = (DS) proceedingJoinPoint.getSignature().getDeclaringType().getAnnotation(DS.class); try { // 寫入線程上下文,應該用哪一個DB DSTypeContainer.setDataBaseType(ds == null ? null : ds.value()); return proceedingJoinPoint.proceed(); } finally { // 清空上下文信息 DSTypeContainer.clearDataBaseType(); } } }
接下來就是比較關鍵的數據源配置了,咱們如今須要註冊DynamicDataSource
,而後將他提供給SqlSessionFactory
,在這裏,咱們但願解決即使多加數據源也不須要修改配置,因此咱們調整了一下數據源的配置結構
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
類的實現方式就相對明確了(建議對比上一篇博文中的配置類)
@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); // 設置默認的數據庫,下面這個賦值方式寫法不太推薦,這裏只是爲了方便而已 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(); } }
項目結構圖
全部前面的東西屬於通用配置相關,接下來給出具體的數據庫操做相關實體類、Mapper 類
數據庫實體類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); }
對應的 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文件 內容基本一致-->
數據庫操做封裝類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); } }
最後簡單的測試下,動態數據源切換是否生效
@SpringBootApplication public class Application { public Application(StoryMoneyRepository storyMoneyRepository, TestMoneyRepository testMoneyRepository) { storyMoneyRepository.query(); testMoneyRepository.query(); } public static void main(String[] args) { SpringApplication.run(Application.class); } }
輸出日誌以下
本文主要給出了一種基於AbstractRoutingDataSource
+ AOP
實現動態數據源切換的實現方式,使用了下面三個知識點
AbstractRoutingDataSource
實現動態數據源切換@DS
註解 + AOP 指定 Mapper 對應的數據源ConfigurationProperties
方式支持添加數據源無需修改配置相關博文
源碼
盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛