【SpringBoot DB 系列】Mybatis 基于 AbstractRoutingDataSource 與 AOP 實現(xiàn)多數(shù)據(jù)源切換

image

【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ù)庫teststory,兩個庫下都存在一個表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;
image

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)圖

image

所有前面的東西屬于通用配置相關(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);
    }
}

輸出日志如下

image

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)博文

源碼

1. 一灰灰 Blog

盡信書則不如,以上內(nèi)容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發(fā)現(xiàn) bug 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的個人博客,記錄所有學(xué)習(xí)和工作中的博文,歡迎大家前去逛逛

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容