春節將至,今天放假了,在此祝小伙伴們新春大吉,身體健康,思路清晰,永遠無BUG!
一句話概括:參數化變更源意思是根據參數動態添加數據源以及切換數據源,解決不確定數據源的問題。
1. 引言
經過前面兩篇文章對於 Spring Boot 處理多個數據庫的策略講解,相信大家已經對多數據源和動態數據源有了比較好的了解。如需回顧,請見:
在前面文章中,留了一個思考題,無論是多套源還是動態數據源,相對來說還是固定的數據源(如一主一從,一主多從等),即在編碼時已經確定的數據庫數量,只是在具體使用哪一個時進行動態處理。如果數據源本身並不確定,或者說需要根據用戶輸入來連接數據庫,這時,如何處理呢?可以想象現在我們有一個需求,需要對數據庫進行連接管理,用戶可以輸入對應的數據庫連接信息,然后可以查看數據庫有哪些表。這就跟平時使用的數據庫管理軟件有點類似了,如 MySQL Workbench、Navicat、SQLyog,下圖是SQLyog截圖:
本文基於前面的示例,添加一個功能,根據用戶輸入的數據庫連接信息,連接數據庫,並返回數據庫的表信息。內容包括動態添加數據源、動態代理簡化數據源操作等。
本文所涉及到的示例代碼:https://github.com/mianshenglee/my-example/tree/master/multi-datasource
,讀者可結合一起看。
2. 參數化變更源說明
2.1 解決思路
Spring Boot 的動態數據源,本質上是把多個數據源存儲在一個 Map 中,當需要使用某個數據源時,從 Map 中獲取此數據源進行處理。在動態數據源處理時,通過繼承抽象類 AbstractRoutingDataSource
可實現此功能。既然是 Map ,如果有新的數據源,把新的數據源添加到此 Map 中就可以了。這就是整個解決思路。
但是,查看 AbstractRoutingDataSource
源碼,可以發現,存放數據源的 Map targetDataSources
是 private 的,而且並沒有提供對此 Map 本身的操作,它提供的是兩個關鍵操作:setTargetDataSources
及 afterPropertiesSet
。其中 setTargetDataSources
設置整個 Map 目標數據源,afterPropertiesSet
則是對 Map 目標數據源進行解析,形成最終使用的 resolvedDataSources
,可見以下源碼:
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 流程說明
- 用戶輸入數據庫連接參數(包括IP、端口、驅動名、數據庫名、用戶名、密碼)
- 根據數據庫連接參數創建數據源
- 添加數據源到動態數據源中
- 切換數據源
- 操作數據庫
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 中
- 添加新數據源時,依然使用
setTargetDataSources
及afterPropertiesSet
完成新數據源添加。- 注意:
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. 總結
本文基於動態數據源,對參數化變更數據源及應用場景進行了說明,提出連接數據庫,查詢表信息的功能需求作為示例,實現根據參數構建數據源,動態添加數據源功能,對參數化變更數據源的使用進行講解,最后使用動態代理簡化操作。本篇文章偏重代碼實現,小伙伴們可以新手實踐來加深認知。
本文配套的示例,示例代碼,有興趣的可以運行示例來感受一下。
參考資料
- Spring主從數據庫的配置和動態數據源切換原理:
https://www.liaoxuefeng.com/article/1182502273240832
- 談談Spring Boot 數據源加載及其多數據源簡單實現:
https://juejin.im/post/5cb0023d5188250df17d4ffc
往期文章
- 搞定SpringBoot多數據源(2):動態數據源
- 搞定SpringBoot多數據源(1):多套源策略
- java開發必學知識:動態代理
- 2019 讀過的好書推薦
- springboot+apache前后端分離部署https
- springboot+logback 日志輸出企業實踐(下)
- springboot+logback 日志輸出企業實踐(上)
我的公眾號(搜索Mason技術記錄
),獲取更多技術記錄: