專案要實現多資料來源動態切換,咋搞?

為何不是夢發表於2020-10-19

文章首發於公眾號:程式設計大道

 

在做專案的時候,幾乎都會用到資料庫,很多時候就只連一個資料庫,但是有時候我們需要一個專案操作多個資料庫,不同的業務功能產生的資料存到不同的資料庫,那怎麼來實現資料來源的動態、靈活的切換呢?今天我們就來實現這個功能。

前期準備工作

我們需要有一臺聯網的電腦(用於maven自動下載依賴),並且電腦安裝JDK 8、IDEA、MySQL資料庫、maven,首先建立一個springboot專案(SSM也行)。springboot版本和SSM版本的程式碼都已經放到碼雲託管,表結構SQL也有,感興趣的可以去下載https://gitee.com/itwalking/springboot-dynamic-datasourcehttps://gitee.com/itwalking/ssm-dynamic-datasource

實現思路

首先講一下我們的實現思路,平時我們做專案,都會用到spring來整合我們的資料來源,連線mysqlOracle資料庫,通過暴露出DataSource相關的介面,然後不同的資料庫都可以整合過來,我們只需要配置資料來源的四大引數即可,這是我們往常的做法。而如果使用動態資料來源的話,Spring也為我們提供了相應的擴充套件點,那就是AbstractRoutingDataSource抽象類,它同樣是jdbcDataSource介面的實現類。

程式碼

廢話不多說,我們直接上程式碼。
建立我們自己的資料來源DynamicDataSource繼承AbstractRoutingDataSource,實現它的抽象方法determineCurrentLookupKey,這個方法其實就是實現動態選擇資料來源的關鍵,通過這個方法返回的物件關聯到我們的資料來源。(已對這個類做了一點優化,具體程式碼在碼雲託管https://gitee.com/itwalking/springboot-dynamic-datasourcehttps://gitee.com/itwalking/ssm-dynamic-datasource

package com.walking.db;
 
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
 
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
 
/**
 * @author walking
 * 公眾號:程式設計大道
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * ThreadLocal 用於提供執行緒區域性變數,在多執行緒環境可以保證各個執行緒裡的變數獨立於其它執行緒裡的變數。
     * 也就是說 ThreadLocal 可以為每個執行緒建立一個【單獨的變數副本】,相當於執行緒的 private static 型別變數。
     */
    private static final ThreadLocal<DataSourceName> dataSourceName = new ThreadLocal<DataSourceName>();
    /**
     * 支援以包名的粒度選擇資料來源
     */
    private static final Map<String,DataSourceName> packageDataSource = new HashMap<>();
 
    public DynamicDataSource(DataSource firstDataSource, Map<Object, Object> targetDataSources) {
        setDefaultTargetDataSource(firstDataSource);
        setTargetDataSources(targetDataSources);
        afterPropertiesSet();
    }
 
    /**
     * 獲取與執行緒上下文繫結的資料來源名稱(儲存在ThreadLocal中)
     * @return 返回資料來源名稱
     */
    @Override
    protected Object determineCurrentLookupKey() {
        DataSourceName dsName = dataSourceName.get();
        dataSourceName.remove();
        return dsName;
    }
    public static void setDataSourceName(DataSourceName dataSource){
        dataSourceName.set(dataSource);
    }
    public static void usePackageDatasourceKey(String pkName) {
        dataSourceName.set(packageDataSource.get(pkName));
    }
    public Map<String,DataSourceName> getPackageDatasource(){
        return packageDataSource;
    }
    public void setPackageDatasource(Map<String,DataSourceName> packageDatasource){
        this.packageDataSource.putAll(packageDatasource);
    }
}

 

DynamicDataSource中有一個ThreadLocal用來儲存我們當前選擇的資料來源名稱,程式碼中的註釋寫的很清楚了。其中ThreadLocal的泛型是DataSourceNameDataSourceName是我們自己定義的一個列舉類,用於定義我們的資料來源名稱,我這裡拿兩個資料來源做演示,並命名為FIRSTSECOND

package com.walking.db;
/**
 * @author walking
 * 公眾號:程式設計大道
 */
public enum DataSourceName {
    FIRST, SECOND;
}

 

然後自定義一個註解,用於標註我們運算元據庫時選擇哪個資料來源,很簡單隻有一個name屬性,預設是 DataSourceName.FIRST

@Target({ElementType.PACKAGE,ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurDataSource {
 
    /**
     * name of DataSource
     * @return
     */
    DataSourceName value() default DataSourceName.FIRST;
 
}

 

有了這些還不夠,我們還需要根據我們的註解裡的name屬性動態的去修改 DynamicDataSource 中 ThreadLocal 中儲存的資料庫名稱,每次執行SQL前都要修改資料來源,這樣才能達到修改資料來源的目的。那很顯然我們就需要spring AOP來完成這個操作了。

如下,DynamicDataSourceAspect 是我們定義的一個切面類,同時也定了三個切點,分別去切方法上帶@CurDataSource註解的方法,類上帶@CurDataSource註解的類,以及按包名去切。這樣,我們的動態資料來源就支援方法級別的、類級別的、包級別的動態配置了。

package com.walking.aaspect;
 
import com.walking.db.CurDataSource;
import com.walking.db.DataSourceName;
import com.walking.db.DynamicDataSource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
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.lang.reflect.Method;
import java.util.Objects;
 
/**
 * 動態資料來源切面類
 * 被切中的,則先判斷方法上是否有CurDataSource註解
 * 然後判斷方法所屬類上是否有CurDataSource註解
 * 其次判斷是否配置了包級別的資料來源
 *
 * 優先順序為方法、類、包
 * 若同時配置則優先按方法上的
 *
 * @author walking
 * 公眾號:程式設計大道
 */
@Slf4j
@Aspect
@Component
public class DynamicDataSourceAspect {
    // pointCut
    @Pointcut("@annotation(com.walking.db.CurDataSource)")
    public void choseDatasourceByAnnotation() {
    }
    @Pointcut("@within(com.walking.db.CurDataSource)")
    public void choseDatasourceByClass() {
    }
    @Pointcut("execution(* com.walking.service3..*(..))")
    public void choseDatasourceByPackage() {
    }
 
    @Around("choseDatasourceByAnnotation() || choseDatasourceByClass() || choseDatasourceByPackage()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("進入AOP環繞通知");
        Signature signature = joinPoint.getSignature();
        DataSourceName datasourceName = getDatasourceKey(signature);
        if (!Objects.isNull(datasourceName)) {
            DynamicDataSource.setDataSourceName(datasourceName);
        }
        return joinPoint.proceed();
    }
    private DataSourceName getDatasourceKey(Signature signature) {
        if (signature == null) {
            return null;
        } else {
            if (signature instanceof MethodSignature) {
                MethodSignature methodSignature = (MethodSignature) signature;
                Method method = methodSignature.getMethod();
                if (method.isAnnotationPresent(CurDataSource.class)) {
                    return this.dsSettingInMethod(method);
                }
                Class<?> declaringClass = method.getDeclaringClass();
                if (declaringClass.isAnnotationPresent(CurDataSource.class)) {
                    return this.dsSettingInConstructor(declaringClass);
                }
                Package aPackage = declaringClass.getPackage();
                this.dsSettingInPackage(aPackage);
            }
            return null;
        }
    }
    private DataSourceName dsSettingInConstructor(Class<?> declaringClass) {
        CurDataSource dataSource = declaringClass.getAnnotation(CurDataSource.class);
        return dataSource.value();
    }
    private DataSourceName dsSettingInMethod(Method method) {
        CurDataSource dataSource = method.getAnnotation(CurDataSource.class);
        return dataSource.value();
    }
    private void dsSettingInPackage(Package pkg) {
        DynamicDataSource.usePackageDatasourceKey(pkg.getName());
    }
}

 

仔細看一下這個切面類的環繞通知這個方法的邏輯,可以發現,我們首先看的是方法上的註解,然後再看類上的註解,最後看是否配置了包級別資料來源。

基本上,該有的類我們都寫完了,剩下就是驗證。

驗證之前我們還需要進行一些配置。

配置多資料來源

這裡,我們使用的是阿里的Druid資料來源,用springboot自帶的也行。我們可以看到在Druid:配置下,原本直接就配置url、name這些引數,我們新增了一級分別是first和second,用於配置多個資料來源

server:
  port: 9966
  servlet:
    context-path: /walking
spring:
  mvc:
    log-request-details: false
  application:
    name: walking
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      first:
        url: jdbc:mysql://localhost:3306/walking_mybatis?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
        username: root
        password: 123456ppzy,
      second:
        url: jdbc:mysql://localhost:3306/walking_mybatis2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
        username: root
        password: 123456ppzy,
 
mybatis:
  mapper-locations: classpath:mapper/*.xml

 

然後javaconfig配置類。
配置了三個bean,前兩個是資料來源的bean,使用@ConfigurationProperties註解,讓springboot幫我們去配置檔案讀取指定字首的配置,這樣我們剛才配的兩個資料來源引數就區分開了。
然後第三個bean是我們配置的叫做dataSource的bean,用於覆蓋spring預設的DataSource,在這個bean中,我們把所有的資料來源注入進去,這裡我們有兩個,命名為FIRST和SECOND(DataSourceName列舉類),以及我們要配置的包級別的資料來源,然後呼叫建構函式建立DynamicDataSource我們的動態資料來源。並指明瞭預設的資料來源。

package com.walking.configuration;
 
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.walking.db.DataSourceName;
import com.walking.db.DynamicDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
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;
 
/**
 * @author walking
 * 公眾號:程式設計大道
 */
@Configuration
public class DynamicDataSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.druid.first")
    public DataSource firstDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    @Bean
    @ConfigurationProperties("spring.datasource.druid.second")
    public DataSource secondDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    @Bean
    @Primary
    public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>(2);
        targetDataSources.put(DataSourceName.FIRST, firstDataSource);
        targetDataSources.put(DataSourceName.SECOND, secondDataSource);
 
        //配置包級別的資料來源
        Map<String, DataSourceName> packageDataSource = new HashMap<>();
        packageDataSource.put("com.walking.service3", DataSourceName.SECOND);
 
        DynamicDataSource dynamicDataSource new DynamicDataSource(firstDataSource, targetDataSources);
        dynamicDataSource.setPackageDatasource(packageDataSource);
        dynamicDataSource.afterPropertiesSet();
        return dynamicDataSource;
    }
}

 

然後就是我們的啟動類了,我們需要禁用掉spring的自動配置資料來源,和Druid的自動配置資料來源,使用我們自定義的動態資料來源。

@EnableAspectJAutoProxy
//關掉資料來源自動配置
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
        DruidDataSourceAutoConfigure.class})
//使用Import註解匯入我們自己的資料來源配置 或在DynamicDataSourceConfig上加Configuration註解
//@Import({DynamicDataSourceConfig.class})
@MapperScan(basePackages = "com.walking.dao")
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

 

操作Mybatis我就不多說了,這裡我在兩個資料庫(walking_mybatis和walking_mybatis2)裡建立了相同的user表,我們測試的時候觀察插入到哪個表就OK了。

專案整體結構

測試

我們在save_1上新增註解指明使用SECOND,在save_2則沒有,UserService1類上也沒用註解,同樣的,在配置類裡也沒配置UserService1的包名,那麼save_2將會使用預設的資料來源那就是FIRST

controller

執行,訪問http://localhost:9966/walking/test01
日誌輸出

檢視資料庫則第二個資料庫新增一條資料。
完整程式碼我已上傳gitee碼雲,詳細的測試都在這三個service包下和test包下,感興趣的可以去下載程式碼看看。

實現動態資料來源切換就是這麼簡單。下次我們看一下動態資料來源的原理。

總結一下

1、繼承AbstractRoutingDataSource實現多資料來源及預設資料來源的配置
2、註解+AOP,實現動態修改資料來源的邏輯
3、排除spring和Druid(如果引入了第三方資料庫連線池)預設的自動配置資料來源

動手操作下一下,SQL和專案都已上傳。

歡迎關注公眾號:程式設計大道

相關文章