花了半天時間,使用spring-boot實現動態資料來源,切換自如

2023-06-12 15:02:00

  在一個專案中使用多個資料來源的情況很多,所以動態切換資料來源是專案中標配的功能,當然網上有相關的依賴可以使用,比如動態資料來源,其依賴為,

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>3.5.1</version>
</dependency>

  今天,不使用現成的API,手動實現一個動態資料來源。

一、環境及依賴

springboot、mybatis-plus的基礎上實現動態資料來源切換,

springboot:2.3.3.RELEASE

mybatis-plus-boot-starter:3.5.0

mysql驅動:8.0.32

除了這些依賴外沒有其他的,目標是動態切換資料來源。

二、實現思路

  先來看下,單資料來源的情況。

  在使用springboot和mybatis-plus時,我們沒有設定資料來源(DataSource),只設定了資料庫相關的資訊,便可以連線資料庫進行資料庫的操作,這是為什麼吶。其實是基於spring-boot的自動設定,也就是autoConfiguration,在自動設定下有DataSourceAutoConfiguration類,該類會生成一個資料來源並注入到spring的容器中,這樣就可以使用該資料來源提供的連線,存取資料庫了。

  感興趣的小夥伴可以瞭解下這個類的具體實現邏輯。

  要實現多資料來源,並且可以自動切換。那麼肯定就不能再使用DataSourceAutoConfigurtation了,因為它只能產生一個資料來源,多個資料來源要怎麼辦,spring提供了AbstractRoutingDataSource類,該類是一個抽象類,僅有一個抽象方法需要實現

Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context.
Allows for arbitrary keys. The returned key needs to match the stored lookup key type, 
as resolved by the resolveSpecifiedLookupKey method.
@Nullable
protected abstract Object determineCurrentLookupKey();

可以根據該類實現一個動態資料來源。好了,現在瞭解了實現思路,開始實現一個動態資料來源,要做以下的準備工作。

1、組態檔;

2、自定義動態資料來源;

2.1、組態檔

由於是多資料來源,那麼在組態檔中肯定是多個設定,不能再是一個資料庫的設定了,這裡使用兩個mysql的設定進行演示,

#master 預設資料來源
spring:
  datasource:
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
      username: root
      password: 123456
#slave 從資料來源
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://127.0.0.1:3306/test2?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
      username: root
      password: 123456

這裡使用了一個master一個slave兩個資料來源設定,其地址是一致的,但資料庫範例不一樣。 有了資料來源的資訊下一步要實現自己的資料來源,

2.2、自定義動態資料來源

  前邊說,spring提供了AbstractRoutingDataSource類可以實現動態資料來源,看下實現。

DynamicDatasource.java

package com.wcj.my.config.dynamic.source;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 動態資料來源
 * @date 2023/6/8 19:18
 */
public class DynamicDatasource extends AbstractRoutingDataSource {
    /**
     * Determine the current lookup key. This will typically be
     * implemented to check a thread-bound transaction context.
     * <p>Allows for arbitrary keys. The returned key needs
     * to match the stored lookup key type, as resolved by the
     * {@link #resolveSpecifiedLookupKey} method.
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDatasourceHolder.getDataSource();
    }
}

這裡的determineCurrentLookupKey方法,需要返回一個資料來源,也就是說返回一個資料來源的對映,這裡返回一個DynamicDatasourceHolder.getDataSource()方法的返回值,DynamicDatasourceHolder是一個儲存多個資料來源的地方,

DynamicDatasourceHolder.java

package com.wcj.my.config.dynamic.source;

import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;

/**
 * @date 2023/6/8 19:42
 */
public class DynamicDatasourceHolder {
    //儲存資料來源的對映
    private static Queue<String> queue = new ArrayBlockingQueue<String>(1);
    public static String getDataSource() {
        return queue.peek();
    }
    public static void setDataSource(String dataSourceKey) {
        queue.add(dataSourceKey);
    }
    public static void removeDataSource(String dataSourceKey) {
        queue.remove(dataSourceKey);
    }
}

該類很簡單,使用一個佇列儲存資料來源的對映,提供獲取/設定資料來源的方法。

這裡使用ThreadLocal類更合適,這樣可以實現執行緒的隔離,一個請求會有一個執行緒來處理,保證每隔執行緒使用的資料來源是一樣的。

到現在為止依舊沒有出現如何建立多資料來源,下面就來了,不著急。

DynamicDatasourceConfig.java

package com.wcj.my.config.dynamic.source;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
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;

/**
 * @date 2023/6/8 19:51
 */
@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class DynamicDatasourceConfig {
    @Bean("master")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDatasource(){
        return DataSourceBuilder.create().build();
    }
    @Bean("slave")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDatasource(){
        return DataSourceBuilder.create().build();
    }
    @Bean
    @Primary
    public DataSource dataSource(){
        Map<Object, Object> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put("master", masterDatasource());
        dataSourceMap.put("slave", slaveDatasource());

        DynamicDatasource dynamicDatasource=new DynamicDatasource();
        dynamicDatasource.setTargetDataSources(dataSourceMap);
        dynamicDatasource.setDefaultTargetDataSource(masterDatasource());
        return dynamicDatasource;
    }
}

首先,在該類上有個一個@Configuration註解,標明這是一個設定類;

其次,有一個@EnableAutonConfiguration註解,該註解中有個陣列型別的exclude屬性,排除不需要自動設定的類,這裡排除的是當然就是DataSourceAutoConfiguration類了;因為下面會自動生成資料來源,不需要自動設定了;

然後,在類中是標有@Bean的方法,這些方法便是生成資料來源類,且對映為」master「、」slave「,可以有多個。使用的是DataSourceBuilder類幫助生成;

最後,生成一個DynamicDatasource,且標有@Primary註解,這裡需要設定」master「、」slave「兩個對映代表的資料來源;

這樣便向spring容器中注入了三個資料來源,分別是」master「、」slave「代表的資料來源,他們是需要實際使用的資料來源。還有一個是DynamicDatasource,提供資料來源的設定。這三個都是DataSource的子類。

三、使用多資料來源

  上面已經完成了多資料來源的設定,下面看怎麼使用吧,還記得DynamicDatasourceHolder類中有set/get方法嗎,就是使用這個類提供的方法,

UserSerivce.java

package com.wcj.my.service;

import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
import com.wcj.my.dto.UserDto;
import com.wcj.my.entity.User;
import com.wcj.my.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @date 2023/6/8 15:19
 */
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    /**預設使用master資料來源
    */
    public boolean saveUser(UserDto userDto) {

        User user = new User();
        user.setUName(userDto.getName());
        user.setUCode(userDto.getCode());
        user.setUAge(userDto.getAge());
        user.setUAddress(userDto.getAddress());
        int num = userMapper.insert(user);
        if (num > 0) {
            return true;
        }
        return false;
    }
    /**
     *使用slave資料來源
     */
    public boolean saveUserSlave(UserDto userDto) {
        DynamicDatasourceHolder.setDataSource("slave");
        User user = new User();
        user.setUName(userDto.getName());
        user.setUCode(userDto.getCode());
        user.setUAge(userDto.getAge());
        user.setUAddress(userDto.getAddress());
        int num = userMapper.insert(user);
        DynamicDatasourceHolder.removeDataSource("slave");
        if (num > 0) {
            return true;
        }
        return false;
    }
}

  上面的service層方法在呼叫dao層方法的時候,使用DynamicDatasourceHolder.setDataSource()方法設定了需要使用的資料來源, 通過這樣的方式便可以實現動態資料來源了。

  不知道,小夥伴們有沒有感覺到,這樣每次在呼叫方法的時候都需要設定資料來源是不是很麻煩,有沒有一種更方面的方式,比如說註解。

四、動態資料來源註解@DDS

   現在來實現一個動態資料來源的註解來代替上面的每次都呼叫DynamicDatasourceHolder.setDataSource()方法來設定資料來源。

  先看下,@DDS註解的定義

DDS.java

package com.wcj.my.config.dynamic.source.aspect;

import org.springframework.stereotype.Component;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**動態資料來源的註解
 * 用在類和方法上,方法上的優先順序大於類上的
 * 預設值是master
 * @date 2023/6/9 16:19
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface DDS {
    String value() default "master";
}

註解@DDS使用在類和方法上,切方法上的優先順序大於類上的。有一個value的屬性,指明使用的資料來源,預設是」master「。

實現一個切面,來切@DDS註解

 DynamicDatasourceAspect.java

package com.wcj.my.config.dynamic.source.aspect;

import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
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.stereotype.Component;

import java.util.Objects;

/**
 * 動態資料來源切面
 * @date 2023/6/9 16:23
 */
@Aspect
@Component
public class DynamicDatasourceAspect {
    /**
     * 切點,切的是帶有@DDS的註解
     */
    @Pointcut("@annotation(com.wcj.my.config.dynamic.source.aspect.DDS)")
    public void dynamicDatasourcePointcut(){

    }
    /**
     * 環繞通知
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("dynamicDatasourcePointcut()")
    public Object around(ProceedingJoinPoint joinPoint)throws Throwable{
        String datasourceKey="master";

        //類上的註解
        Class<?> targetClass=joinPoint.getTarget().getClass();
        DDS annotation=targetClass.getAnnotation(DDS.class);

        //方法上的註解
        MethodSignature methodSignature=(MethodSignature)joinPoint.getSignature();
        DDS annotationMethod=methodSignature.getMethod().getAnnotation(DDS.class);
        if(Objects.nonNull(annotationMethod)){
            datasourceKey=annotationMethod.value();
        }else{
            datasourceKey=annotation.value();
        }
        //設定資料來源
        DynamicDatasourceHolder.setDataSource(datasourceKey);
        try{
           return joinPoint.proceed();
        }finally {
            DynamicDatasourceHolder.removeDataSource(datasourceKey);
        }
    }
}

 這樣一個動態資料來源的註解便可以了,看下怎麼使用,

UserServiceByAnnotation.java

package com.wcj.my.service;

import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
import com.wcj.my.config.dynamic.source.aspect.DDS;
import com.wcj.my.dto.UserDto;
import com.wcj.my.entity.User;
import com.wcj.my.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @date 2023/6/8 15:19
 */
@Service
public class UserServiceByAnnotation {
    @Autowired
    private UserMapper userMapper;
    @DDS("master")
    public boolean saveUser(UserDto userDto){
        User user=new User();
        user.setUName(userDto.getName());
        user.setUCode(userDto.getCode());
        user.setUAge(userDto.getAge());
        user.setUAddress(userDto.getAddress());
        int num=userMapper.insert(user);
        if(num>0){
            return true;
        }
        return false;
    }
    @DDS("slave")
    public boolean saveUserSlave(UserDto userDto){
        User user=new User();
        user.setUName(userDto.getName());
        user.setUCode(userDto.getCode());
        user.setUAge(userDto.getAge());
        user.setUAddress(userDto.getAddress());
        int num=userMapper.insert(user);
        if(num>0){
            return true;
        }
        return false;
    }
}

使用起來很簡單,在需要切換資料來源的方法或類上使用@DDS註解即可,使用value來改變資料來源就好了。

五、動態資料來源的原理

  很多小夥伴可能和我有一樣的疑惑,使用DynamicDatasourceHolder.setDataSource或@DDS就可以設定資料來源了,是怎麼實現的,下面分析下,我們指定dao層的Mapper其實是一個代理物件,其會使用mybatis中的sqlSessionTempalte進行資料庫的操作,在sqlSessionTemplate中會使用DefaultSqlSession物件,最終會使用DataSource,而使用了動態資料來源的物件中會注入一個DynamicDataSource,在進行資料庫操作時最終會獲得一個資料庫連線,這裡便會使用DynamicDataSource獲得一個連線,由於它繼承了AbstractRoutingDataSource類,看下其getConnection方法,

@Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}

看下determineTargetDataSource()方法,

protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");、
          //自己實現的,在呼叫方法時進行了設定,實現動態資料來源的目的    
		Object lookupKey = determineCurrentLookupKey();
		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 + "]");
		}
		return dataSource;
	}

看上面的註釋,determineCurrentLookupkey()方法便是在DynamicDatasource類中進行了實現,從而實現了動態設定資料來源的目的。

六、總結 

本文動手實現了一個動態資料來源,並切提供了註解的方式,主要有以下幾點

1、繼承AbstractRoutingDataSource類的determineCurrentLookupkey()方法,動態設定資料來源;

2、取消DataSourceAutoConfiguration的自動設定,手動向spring容器中注入多個資料來源;

3、基於@DDS註解動態設定資料來源;

最後,本文用到的原始碼均可通過下方公眾號獲得。