【SpringBoot DB 系列】Mybatis 基於 AOP 實現多數據源切換

【SpringBoot DB 系列】Mybatis 基於 AbstractRoutingDataSource 與 AOP 實現多數據源切換

前面一篇博文介紹了 Mybatis 多數據源的配置,簡單來說就是一個數據源一個配置指定,不一樣數據源的 Mapper 分開指定;本文將介紹另一種方式,藉助AbstractRoutingDataSource來實現動態切換數據源,並經過自定義註解方式 + AOP 來實現數據源的指定java

<!-- more -->mysql

I. 環境準備

1. 數據庫相關

以 mysql 爲例進行演示說明,由於須要多數據源,一個最簡單的 case 就是一個物理庫上多個邏輯庫,本文是基於本機的 mysql 進行操做git

建立數據庫teststory,兩個庫下都存在一個表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;

2. 項目環境

本項目藉助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

II. 多數據源配置

強烈建議沒有看上一篇博文的小夥伴,先看一下上篇博文 【DB 系列】Mybatis 多數據源配置與使用

在開始以前,先有必要回顧一下以前 Mybatis 多數據源配置的主要問題在哪裏微信

  • 多加一個數據源,須要多一份配置
  • Mapper 文件須要分包處理,對開發人員而言這是個潛在的坑

針對上面這個,那咱們想實現的目的也很清晰了,解決上面兩個問題mybatis

1. AbstractRoutingDataSource

實現多數據源的關鍵,從名字上就能夠看出,它就是用來路由具體的數據源的,其核心代碼如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須要咱們本身來實現,到底返回哪一個數據源

2. 動態數據源實現

咱們建立一個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();
    }
}

3. 註解實現

上面雖然給出了數據源選擇的策略,從線程上下文中獲取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();
        }
    }
}

4. 註冊配置

接下來就是比較關鍵的數據源配置了,咱們如今須要註冊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();
    }
}

5. 數據庫實體類

項目結構圖

全部前面的東西屬於通用配置相關,接下來給出具體的數據庫操做相關實體類、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);
    }
}

6. 測試

最後簡單的測試下,動態數據源切換是否生效

@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.小結

本文主要給出了一種基於AbstractRoutingDataSource + AOP實現動態數據源切換的實現方式,使用了下面三個知識點

  • AbstractRoutingDataSource實現動態數據源切換
  • 自定義@DS註解 + AOP 指定 Mapper 對應的數據源
  • ConfigurationProperties方式支持添加數據源無需修改配置

II. 其餘

0. 項目

相關博文

源碼

1. 一灰灰 Blog

盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛

一灰灰blog

相關文章
相關標籤/搜索