
一、概述
在項目的開發過程中,遇到了需要從數據庫中動態查詢新的數據源信息并切換到該數據源做相應的查詢操作,這樣就產生了動態切換數據源的場景。為了能夠靈活地指定具體的數據庫,本文基于注解和AOP的方法實現多數據源自動切換。在使用過程中,只需要添加注解就可以使用,簡單方便。(代碼獲取方式:見文章底部(開箱即用))
二、構建核心代碼
2.1、AbstractRoutingDataSource構建
ackage com.wonders.dynamic;import org.springframework.beans.factory.InitializingBean;import org.springframework.jdbc.datasource.AbstractDataSource;import org.springframework.jdbc.datasource.lookup.DataSourceLookup;import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;import org.springframework.lang.Nullable;import org.springframework.util.Assert;import org.springframework.util.CollectionUtils;import JAVAx.sql.DataSource;import java.sql.Connection;import java.sql.SQLException;import java.util.Map;/*** @Description: TODO:抽象類AbstractRoutingDataSource,實現動態數據源切換* @Author: yyalin* @CreateDate: 2023/7/16 14:40* @Version: V1.0*/public abstract class AbstractRoutingDataSource extends AbstractDataSourceimplements InitializingBean {//目標數據源map集合,存儲將要切換的多數據源bean信息@Nullableprivate Map<Object, Object> targetDataSources;//未指定數據源時的默認數據源對象@Nullableprivate Object defaultTargetDataSource;private boolean lenientFallback = true;//數據源查找接口,通過該接口的getDataSource(String dataSourceName)獲取數據源信息private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();//解析targetDataSources之后的DataSource的map集合@Nullableprivate Map<Object, DataSource> resolvedDataSources;@Nullableprivate DataSource resolvedDefaultDataSource;//將targetDataSources的內容轉化一下放到resolvedDataSources中,將defaultTargetDataSource轉為DataSource賦值給resolvedDefaultDataSourcepublic void afterPropertiesSet() {//如果目標數據源為空,會拋出異常,在系統配置時應至少傳入一個數據源if (this.targetDataSources == null) {throw new IllegalArgumentException("Property 'targetDataSources' is required");} else {//初始化resolvedDataSources的大小this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());//遍歷目標數據源信息map集合,對其中的key,value進行解析this.targetDataSources.forEach((key, value) -> {//resolveSpecifiedLookupKey方法沒有做任何處理,只是將key繼續返回Object lookupKey = this.resolveSpecifiedLookupKey(key);//將目標數據源map集合中的value值(德魯伊數據源信息)轉為DataSource類型DataSource dataSource = this.resolveSpecifiedDataSource(value);//將解析之后的key,value放入resolvedDataSources集合中this.resolvedDataSources.put(lookupKey, dataSource);});if (this.defaultTargetDataSource != null) {//將默認目標數據源信息解析并賦值給resolvedDefaultDataSourcethis.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);}}}protected Object resolveSpecifiedLookupKey(Object lookupKey) {return lookupKey;}protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {if (dataSource instanceof DataSource) {return (DataSource)dataSource;} else if (dataSource instanceof String) {return this.dataSourceLookup.getDataSource((String)dataSource);} else {throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);}}//因為AbstractRoutingDataSource繼承AbstractDataSource,而AbstractDataSource實現了DataSource接口,所有存在獲取數據源連接的方法public Connection getConnection() throws SQLException {return this.determ.NETargetDataSource().getConnection();}public Connection getConnection(String username, String password) throws SQLException {return this.determineTargetDataSource().getConnection(username, password);}protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");//調用實現類中重寫的determineCurrentLookupKey方法拿到當前線程要使用的數據源的名稱Object lookupKey = this.determineCurrentLookupKey();//去解析之后的數據源信息集合中查詢該數據源是否存在,如果沒有拿到則使用默認數據源resolvedDefaultDataSourceDataSource 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;}}@Nullableprotected abstract Object determineCurrentLookupKey();}
2.2、DynamicDataSource類
*** @Description: TODO:動態數據源* @Author: yyalin* @CreateDate: 2023/7/16 14:46* @Version: V1.0*//**** 調用AddDefineDataSource組件的addDefineDynamicDataSource()方法,獲取原來targetdatasources的map,* 并將新的數據源信息添加到map中,并替換targetdatasources中的map* 切換數據源時可以使用@DataSource(value = "數據源名稱"),或者DynamicDataSourceContextHolder.setContextKey("數據源名稱")*/@Data@AllArgsConstructor@NoArgsConstructorpublic class DynamicDataSource extends AbstractRoutingDataSource {//備份所有數據源信息,private Map<Object, Object> defineTargetDataSources;/*** 決定當前線程使用哪個數據源*/@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceHolder.getDynamicDataSourceKey();}}
2.3、DynamicDataSourceHolder
*** @Description: TODO:數據源切換處理* DynamicDataSourceHolder類主要是設置當前線程的數據源名稱,* 移除數據源名稱,以及獲取當前數據源的名稱,便于動態切換* @Author: yyalin* @CreateDate: 2023/7/16 14:51* @Version: V1.0*/@Slf4jpublic class DynamicDataSourceHolder {/*** 保存動態數據源名稱*/private static final ThreadLocal<String> DYNAMIC_DATASOURCE_KEY = new ThreadLocal<>();/*** 設置/切換數據源,決定當前線程使用哪個數據源*/public static void setDynamicDataSourceKey(String key){log.info("數據源切換為:{}",key);DYNAMIC_DATASOURCE_KEY.set(key);}/*** 獲取動態數據源名稱,默認使用mater數據源*/public static String getDynamicDataSourceKey(){String key = DYNAMIC_DATASOURCE_KEY.get();return key == null ? DbsConstant.MySQL_db_01 : key;}/*** 移除當前數據源*/public static void removeDynamicDataSourceKey(){log.info("移除數據源:{}",DYNAMIC_DATASOURCE_KEY.get());DYNAMIC_DATASOURCE_KEY.remove();}}
2.4、數據源工具類
*** @Description: TODO:數據源工具類* @Author: yyalin* @CreateDate: 2023/7/16 15:00* @Version: V1.0*/@Slf4j@Componentpublic class DataSourceUtils {@ResourceDynamicDataSource dynamicDataSource;/*** @Description: 根據傳遞的數據源信息測試數據庫連接* @Author zhangyu*/public DruidDataSource createDataSourceConnection(DataSourceInfo dataSourceInfo) {DruidDataSource druidDataSource = new DruidDataSource();druidDataSource.setUrl(dataSourceInfo.getUrl());druidDataSource.setUsername(dataSourceInfo.getUserName());druidDataSource.setPassword(dataSourceInfo.getPassword());druidDataSource.setDriverClassName(dataSourceInfo.getDriverClassName());druidDataSource.setBreakAfterAcquireFAIlure(true);druidDataSource.setConnectionErrorRetryAttempts(0);try {druidDataSource.getConnection(2000);log.info("數據源連接成功");return druidDataSource;} catch (SQLException throwables) {log.error("數據源 {} 連接失敗,用戶名:{},密碼 {}",dataSourceInfo.getUrl(),dataSourceInfo.getUserName(),dataSourceInfo.getPassword());return null;}}/*** @Description: 將新增的數據源加入到備份數據源map中* @Author zhangyu*/public void addDefineDynamicDataSource(DruidDataSource druidDataSource, String dataSourceName){Map<Object, Object> defineTargetDataSources = dynamicDataSource.getDefineTargetDataSources();defineTargetDataSources.put(dataSourceName, druidDataSource);dynamicDataSource.setTargetDataSources(defineTargetDataSources);dynamicDataSource.afterPropertiesSet();}
2.5、DynamicDataSourceConfig
*** @Description: TODO:數據源信息配置類,讀取數據源配置信息并注冊成bean。* @Author: yyalin* @CreateDate: 2023/7/16 14:54* @Version: V1.0*/@Configuration@MApperScan("com.wonders.mapper")@Slf4jpublic class DynamicDataSourceConfig {@Bean(name = DbsConstant.mysql_db_01)@ConfigurationProperties("spring.datasource.mysqldb01")public DataSource masterDataSource() {log.info("數據源切換為:{}",DbsConstant.mysql_db_01);DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return dataSource;}@Bean(name = DbsConstant.mysql_db_02)@ConfigurationProperties("spring.datasource.mysqldb02")public DataSource slaveDataSource() {log.info("數據源切換為:{}",DbsConstant.mysql_db_02);DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return dataSource;}@Bean(name = DbsConstant.oracle_db_01)@ConfigurationProperties("spring.datasource.oracledb01")public DataSource oracleDataSource() {log.info("數據源切換為oracle:{}",DbsConstant.oracle_db_01);DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return dataSource;}@Bean@Primarypublic DynamicDataSource dynamicDataSource(){Map<Object, Object> dataSourceMap = new HashMap<>(3);dataSourceMap.put(DbsConstant.mysql_db_01,masterDataSource());dataSourceMap.put(DbsConstant.mysql_db_02,slaveDataSource());dataSourceMap.put(DbsConstant.oracle_db_01,oracleDataSource());//設置動態數據源DynamicDataSource dynamicDataSource = new DynamicDataSource();dynamicDataSource.setDefaultTargetDataSource(masterDataSource());dynamicDataSource.setTargetDataSources(dataSourceMap);//將數據源信息備份在defineTargetDataSources中dynamicDataSource.setDefineTargetDataSources(dataSourceMap);return dynamicDataSource;}}
三、測試代碼
/*** @Description: TODO* @Author: yyalin* @CreateDate: 2023/7/16 15:02* @Version: V1.0*/@Slf4j@Api(tags="動態切換多數據源測試")@RestControllerpublic class TestController {@ResourceDataSourceUtils dataSourceUtils;@Autowiredprivate StudentMapper studentMapper;@ApiOperation(value="動態切換多數據源測試", notes="test")@GetMapping("/test")public Map<String, Object> dynamicDataSourceTest(String id){Map<String, Object> map = new HashMap<>();//1、默認庫中查詢數據Student student=studentMapper.selectById(id);map.put("1、默認庫中查詢到的數據",student);//2、指定庫中查詢的數據DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.mysql_db_02);Student student02=studentMapper.selectById(id);map.put("2、指定庫中查詢的數據",student02);//3、從數據庫獲取連接信息,然后獲取數據//模擬從數據庫中獲取的連接DataSourceInfo dataSourceInfo = new DataSourceInfo("jdbc:mysql://127.0.0.1:3308/test02?useUnicode=true&characterEncoding=utf-8&useSSL=false","root","root","mysqldb03","com.mysql.cj.jdbc.Driver");map.put("dataSource",dataSourceInfo);log.info("數據源信息:{}",dataSourceInfo);//測試數據源連接DruidDataSource druidDataSource = dataSourceUtils.createDataSourceConnection(dataSourceInfo);if (Objects.nonNull(druidDataSource)){//將新的數據源連接添加到目標數據源map中dataSourceUtils.addDefineDynamicDataSource(druidDataSource,dataSourceInfo.getDatasourceKey());//設置當前線程數據源名稱-----代碼形式DynamicDataSourceHolder.setDynamicDataSourceKey(dataSourceInfo.getDatasourceKey());//在新的數據源中查詢用戶信息Student student03=studentMapper.selectById(id);map.put("3、動態數據源查詢的數據",student03);//關閉數據源連接druidDataSource.close();}//4、指定oracle庫中查詢的數據DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.oracle_db_01);Student student04=studentMapper.selectById(id);map.put("4、指定oracle庫中查詢的數據",student04);return map;}}
測試結果如下:
從結果中可以明顯的看出,通過切換不同的數據源,可以從不同的庫中獲取不同的數據,包括:常見庫Mysql、oracle、sqlserver等數據庫相互切換。也可以從數據庫的某張表中獲取連接信息,實現動態切換數據庫。

四、使用注解方式切換數據源
從上述TestController 中代碼不難看出,若要想切換數據源需要在mapper調用之前調用:
DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.mysql_db_02);
不夠簡潔優雅,所以下面推薦使用注解的方式來動態進行數據源的切換。
4.1、創建注解類DataSource
/*** @Description: TODO:自定義多數據源切換注解* 優先級:先方法,后類,如果方法覆蓋了類上的數據源類型,以方法的為準,否則以類上的為準* @Author: yyalin* @CreateDate: 2023/7/17 14:00* @Version: V1.0*/@Target({ ElementType.METHOD, ElementType.TYPE })@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface DataSource {//切換數據源名稱,默認mysql_db_01public String value() default DbsConstant.mysql_db_01;}
4.2、創建切面DataSourceAspect類
*** @Description: TODO:創建切面DataSourceAspect類* @Author: yyalin* @CreateDate: 2023/7/17 14:03* @Version: V1.0*/@Aspect@Componentpublic class DataSourceAspect {// 設置DataSource注解的切點表達式@Pointcut("@annotation(com.wonders.dynamic.DataSource)")public void dynamicDataSourcePointCut(){}//環繞通知@Around("dynamicDataSourcePointCut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable{String key = getDefineAnnotation(joinPoint).value();DynamicDataSourceHolder.setDynamicDataSourceKey(key);try {return joinPoint.proceed();} finally {DynamicDataSourceHolder.removeDynamicDataSourceKey();}}/*** 功能描述:先判斷方法的注解,后判斷類的注解,以方法的注解為準* @MethodName: getDefineAnnotation* @MethodParam: [joinPoint]* @Return: com.wonders.dynamic.DataSource* @Author: yyalin* @CreateDate: 2023/7/17 14:09*/private DataSource getDefineAnnotation(ProceedingJoinPoint joinPoint){MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();DataSource dataSourceAnnotation = methodSignature.getMethod().getAnnotation(DataSource.class);if (Objects.nonNull(methodSignature)) {return dataSourceAnnotation;} else {Class<?> dsClass = joinPoint.getTarget().getClass();return dsClass.getAnnotation(DataSource.class);}}}
4.3、進行數據源切換
/@Mapper 與 啟動類的@MapperScan({"com.example.demo.mapper"}) 二選一即可@Repositorypublic interface StudentMapper extends BaseMapper<Student> {/*** 功能描述:在mysql_db_01中查詢數據* @MethodName: findStudentById* @MethodParam: [id]* @Return: com.wonders.entity.Student* @Author: yyalin* @CreateDate: 2023/7/17 14:20*/@DataSource(value = DbsConstant.oracle_db_01)Student findStudentById(String id);}
或在service層
Servicepublic class StudentServiceImpl implements StudentService{@Autowiredprivate StudentMapper studentMapper;//注解加在實現層才能生效@DataSource(value = DbsConstant.mysql_db_01)@Overridepublic Student findStudentById(String id) {return studentMapper.selectById(id);}}
4.3、測試效果
ApiOperation(value="使用注解方式動態切換多數據源", notes="test02")@GetMapping("/test02")public Student test02(String id){Student student=studentMapper.findStudentById(id);return student;}
--結果如下:







