搞定SpringBoot多數據源(3):參數化變動源

[toc]html

2020鼠年大吉

春節將至,今天放假了,在此祝小夥伴們新春大吉,身體健康,思路清晰,永遠無BUG!java


一句話歸納:參數化變動源意思是根據參數動態添加數據源以及切換數據源,解決不肯定數據源的問題。mysql

1. 引言

通過前面兩篇文章對於 Spring Boot 處理多個數據庫的策略講解,相信你們已經對多數據源和動態數據源有了比較好的瞭解。如需回顧,請見:git

在前面文章中,留了一個思考題,不管是多套源仍是動態數據源,相對來講仍是固定的數據源(如一主一從,一主多從等),即在編碼時已經肯定的數據庫數量,只是在具體使用哪個時進行動態處理。若是數據源自己並不肯定,或者說須要根據用戶輸入來鏈接數據庫,這時,如何處理呢?能夠想象如今咱們有一個需求,須要對數據庫進行鏈接管理,用戶能夠輸入對應的數據庫鏈接信息,而後能夠查看數據庫有哪些表。這就跟平時使用的數據庫管理軟件有點相似了,如 MySQL Workbench、Navicat、SQLyog,下圖是SQLyog截圖:github

SQLyog

本文基於前面的示例,添加一個功能,根據用戶輸入的數據庫鏈接信息,鏈接數據庫,並返回數據庫的表信息。內容包括動態添加數據源、動態代理簡化數據源操做等。spring

本文所涉及到的示例代碼:https://github.com/mianshenglee/my-example/tree/master/multi-datasource,讀者可結合一塊兒看。sql

2. 參數化變動源說明

2.1 解決思路

Spring Boot 的動態數據源,本質上是把多個數據源存儲在一個 Map 中,當須要使用某個數據源時,從 Map 中獲取此數據源進行處理。在動態數據源處理時,經過繼承抽象類 AbstractRoutingDataSource 可實現此功能。既然是 Map ,若是有新的數據源,把新的數據源添加到此 Map 中就能夠了。這就是整個解決思路。數據庫

可是,查看 AbstractRoutingDataSource 源碼,能夠發現,存放數據源的 Map targetDataSources 是 private 的,並且並無提供對此 Map 自己的操做,它提供的是兩個關鍵操做:setTargetDataSourcesafterPropertiesSet 。其中 setTargetDataSources 設置整個 Map 目標數據源,afterPropertiesSet 則是對 Map 目標數據源進行解析,造成最終使用的 resolvedDataSources,可見如下源碼:apache

this.resolvedDataSources = new HashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
    Object lookupKey = this.resolveSpecifiedLookupKey(key);
    DataSource dataSource = this.resolveSpecifiedDataSource(value);
    this.resolvedDataSources.put(lookupKey, dataSource);
});

所以,爲實現動態添加數據源到 Map 的功能,咱們能夠根據這兩個關鍵操做進行處理。後端

2.2 流程說明

  1. 用戶輸入數據庫鏈接參數(包括IP、端口、驅動名、數據庫名、用戶名、密碼)
  2. 根據數據庫鏈接參數建立數據源
  3. 添加數據源到動態數據源中
  4. 切換數據源
  5. 操做數據庫

3. 實現參數化變動源

說明,下面的操做基於以前文章的示例,基本的工程搭建及配置再也不重複說明,有須要可參考文章。

3.1 改造動態數據源

3.1.1 動態數據源添加功能

爲了能夠動態添加數據源到 Map ,咱們須要對動態數據源進行改造。以下:

public class DynamicDataSource extends AbstractRoutingDataSource {
    private Map<Object, Object> backupTargetDataSources;

    /**
     * 自定義構造函數
     */
    public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSource){
        backupTargetDataSources = targetDataSource;
        super.setDefaultTargetDataSource(defaultDataSource);
        super.setTargetDataSources(backupTargetDataSources);
        super.afterPropertiesSet();
    }

    /**
     * 添加新數據源
     */
    public void addDataSource(String key, DataSource dataSource){
        this.backupTargetDataSources.put(key,dataSource);
        super.setTargetDataSources(this.backupTargetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getContextKey();
    }
}
  • 添加了自定義的 backupTargetDataSources 做爲原 targetDataSources 的拷貝
  • 自定義構造函數,把須要保存的目標數據源拷貝到自定義的 Map 中
  • 添加新數據源時,依然使用 setTargetDataSourcesafterPropertiesSet 完成新數據源添加。
  • 注意:afterPropertiesSet 的做用很重要,它負責解析成可用的目標數據源。

3.1.2 動態數據源配置

原來在建立動態數據源時,使用的是無參數構造函數,通過前面改造後,使用有參構造函數,以下:

@Bean
@Primary
public DataSource dynamicDataSource() {
    Map<Object, Object> dataSourceMap = new HashMap<>(2);
    dataSourceMap.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource());
    dataSourceMap.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource());
    //有參構造函數
    return new DynamicDataSource(masterDataSource(), dataSourceMap);
}

3.2 添加數據源工具類

3.2.1 Spring 上下文工具類

在Spring Boot 使用過程當中,常常會用到 Spring 的上下文,常見的就是從 Spring 的 IOC 中獲取 bean 來進行操做。因爲 Spring 使用的 IOC 基本上把 bean 都注入到容器中,所以須要 Spring 上下文來獲取。咱們在 context 包下添加 SpringContextHolder ,以下:

@Component
public class SpringContextHolder implements ApplicationContextAware {
    private static ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextHolder.applicationContext = applicationContext;
    }
    /**
     * 返回上下文
     */
    public static ApplicationContext getContext(){
        return SpringContextHolder.applicationContext;
    }
}

經過 getContext 就能夠獲取上下文,進而操做。

3.2.2 數據源操做工具

經過參數添加數據源,須要根據參數構造數據源,而後添加到前面說的 Map 中。以下:

public class DataSourceUtil {
    /**
     * 建立新的數據源,注意:此處只針對 MySQL 數據庫
     */
    public static DataSource makeNewDataSource(DbInfo dbInfo){
        String url = "jdbc:mysql://"+dbInfo.getIp() + ":"+dbInfo.getPort()+"/"+dbInfo.getDbName()
                +"?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8";
        String driveClassName = StringUtils.isEmpty(dbInfo.getDriveClassName())? "com.mysql.cj.jdbc.Driver":dbInfo.getDriveClassName();
        return DataSourceBuilder.create().url(url)
                .driverClassName(driveClassName)
                .username(dbInfo.getUsername())
                .password(dbInfo.getPassword())
                .build();
    }

    /**
     * 添加數據源到動態源中
     */
    public static void addDataSourceToDynamic(String key, DataSource dataSource){
        DynamicDataSource dynamicDataSource = SpringContextHolder.getContext().getBean(DynamicDataSource.class);
        dynamicDataSource.addDataSource(key,dataSource);
    }

    /**
     * 根據數據庫鏈接信息添加數據源到動態源中
     * @param key
     * @param dbInfo
     */
    public static void addDataSourceToDynamic(String key, DbInfo dbInfo){
        DataSource dataSource = makeNewDataSource(dbInfo);
        addDataSourceToDynamic(key,dataSource);
    }
}
  • 經過 DataSourceBuilder 及相應的參數來構造數據源,注意此處只針對 MySQL 做處理,其它數據庫的話,對應的 url 及 DriveClassName 需做相應的變動。
  • 添加數據源時,經過 Spring 上下文獲取動態數據源的 bean,而後添加。

3.3 使用參數變動數據源

前面兩步已實現添加數據源,下面咱們根據需求(根據用戶輸入的數據庫鏈接信息,鏈接數據庫,並返回數據庫的表信息),看看如何使用它。

3.3.1 添加查詢數據庫表信息的 Mapper

經過 MySQL 的 information_schema 能夠獲取表信息。

@Repository
public interface TableMapper extends BaseMapper<TestUser> {
    /**
     * 查詢表信息
     */
    @Select("select table_name, table_comment, create_time, update_time " +
            " from information_schema.tables " +
            " where table_schema = (select database())")
    List<Map<String,Object>> selectTableList();
}

3.3.2 定義數據庫鏈接信息對象

把數據庫鏈接信息經過一個類進行封裝。

@Data
public class DbInfo {
    private String ip;
    private String port;
    private String dbName;
    private String driveClassName;
    private String username;
    private String password;
}

3.3.3 參數化變動源並查詢表信息

在 controller 層,咱們定義一個查詢表信息的接口,根據傳入的參數,鏈接數據源,返回表信息:

/**
 * 根據數據庫鏈接信息獲取表信息
 */
@GetMapping("table")
public Object findWithDbInfo(DbInfo dbInfo) throws Exception {
    //數據源key
    String newDsKey = System.currentTimeMillis()+"";
    //添加數據源
    DataSourceUtil.addDataSourceToDynamic(newDsKey,dbInfo);
    DynamicDataSourceContextHolder.setContextKey(newDsKey);
    //查詢表信息
    List<Map<String, Object>> tables = tableMapper.selectTableList();
    DynamicDataSourceContextHolder.removeContextKey();
    return ResponseResult.success(tables);
}
  • 訪問地址 http://localhost:8080/dd/table?ip=localhost&port=3310&dbName=mytest&username=root&password=111111 ,對應數據庫鏈接參數。
  • 此處數據源的 key 是無心義的,建議根據實際場景設置有意義的值

4. 動態代理消除模板代碼

前面已經完成了參數化切換數據源功能,但還有一點就是有模板代碼,如添加數據源、切換數據源、對此數據源進行CURD操做、釋放數據源,若是每一個地方都這樣作,就很繁瑣,這個時候,就須要用到動態代理了,可參數我以前的文章:java開發必學知識:動態代理。此處,使用 JDK 自帶的動態代理,實現參數化變動數據源的功能,消除模板代碼。

4.1 添加 JDK 動態代理

添加 proxy 包,添加 JdkParamDsMethodProxy 類,實現 InvocationHandler 接口,在 invoke 中編寫參數化切換數據源的邏輯便可。以下:

public class JdkParamDsMethodProxy implements InvocationHandler {
    // 代理對象及相應參數
    private String dataSourceKey;
    private DbInfo dbInfo;
    private Object targetObject;
    public JdkParamDsMethodProxy(Object targetObject, String dataSourceKey, DbInfo dbInfo) {
        this.targetObject = targetObject;
        this.dataSourceKey = dataSourceKey;
        this.dbInfo = dbInfo;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //切換數據源
        DataSourceUtil.addDataSourceToDynamic(dataSourceKey, dbInfo);
        DynamicDataSourceContextHolder.setContextKey(dataSourceKey);
        //調用方法
        Object result = method.invoke(targetObject, args);
        DynamicDataSourceContextHolder.removeContextKey();
        return result;
    }

    /**
     * 建立代理
     */
    public static Object createProxyInstance(Object targetObject, String dataSourceKey, DbInfo dbInfo) throws Exception {
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader()
                , targetObject.getClass().getInterfaces(), new JdkParamDsMethodProxy(targetObject, dataSourceKey, dbInfo));
    }
}
  • 代碼中,須要使用的參數經過構造函數傳入
  • 經過 Proxy.newProxyInstance 建立代理,在方法執行時( invoke ) 進行數據源添加、切換、數據庫操做、清除等

4.2 使用代理實現功能

有了代理,在添加和切換數據源時就能夠擦除模板代碼,前面的業務代碼就變成:

@GetMapping("table")
    public Object findWithDbInfo(DbInfo dbInfo) throws Exception {
        //數據源key
        String newDsKey = System.currentTimeMillis()+"";
        //使用代理切換數據源
        TableMapper tableMapperProxy = (TableMapper)JdkParamDsMethodProxy.createProxyInstance(tableMapper, newDsKey, dbInfo);
        List<Map<String, Object>> tables = tableMapperProxy.selectTableList();
        return ResponseResult.success(tables);
    }

經過代理,代碼就簡潔多了。

5. 總結

本文基於動態數據源,對參數化變動數據源及應用場景進行了說明,提出鏈接數據庫,查詢表信息的功能需求做爲示例,實現根據參數構建數據源,動態添加數據源功能,對參數化變動數據源的使用進行講解,最後使用動態代理簡化操做。本篇文章偏重代碼實現,小夥伴們能夠新手實踐來加深認知。

本文配套的示例,示例代碼,有興趣的能夠運行示例來感覺一下。

參考資料

往期文章

個人公衆號(搜索Mason技術記錄),獲取更多技術記錄:

mason

相關文章
相關標籤/搜索