一、概述
在項(xiàng)目的開(kāi)發(fā)過(guò)程中,遇到了需要從數(shù)據(jù)庫(kù)中動(dòng)態(tài)查詢(xún)新的數(shù)據(jù)源信息并切換到該數(shù)據(jù)源做相應(yīng)的查詢(xún)操作,這樣就產(chǎn)生了動(dòng)態(tài)切換數(shù)據(jù)源的場(chǎng)景。為了能夠靈活地指定具體的數(shù)據(jù)庫(kù),本文基于注解和AOP的方法實(shí)現(xiàn)多數(shù)據(jù)源自動(dòng)切換。在使用過(guò)程中,只需要添加注解就可以使用,簡(jiǎn)單方便。(代碼獲取方式:見(jiàn)文章底部(開(kāi)箱即用))
二、構(gòu)建核心代碼
2.1、AbstractRoutingDataSource構(gòu)建
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:抽象類(lèi)AbstractRoutingDataSource,實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)源切換
* @Author: yyalin
* @CreateDate: 2023/7/16 14:40
* @Version: V1.0
*/
public abstract class AbstractRoutingDataSource extends AbstractDataSource
implements InitializingBean {
//目標(biāo)數(shù)據(jù)源map集合,存儲(chǔ)將要切換的多數(shù)據(jù)源bean信息
@Nullable
private Map<Object, Object> targetDataSources;
//未指定數(shù)據(jù)源時(shí)的默認(rèn)數(shù)據(jù)源對(duì)象
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
//數(shù)據(jù)源查找接口,通過(guò)該接口的getDataSource(String dataSourceName)獲取數(shù)據(jù)源信息
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
//解析targetDataSources之后的DataSource的map集合
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
//將targetDataSources的內(nèi)容轉(zhuǎn)化一下放到resolvedDataSources中,將defaultTargetDataSource轉(zhuǎn)為DataSource賦值給resolvedDefaultDataSource
public void afterPropertiesSet() {
//如果目標(biāo)數(shù)據(jù)源為空,會(huì)拋出異常,在系統(tǒng)配置時(shí)應(yīng)至少傳入一個(gè)數(shù)據(jù)源
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
//初始化resolvedDataSources的大小
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
//遍歷目標(biāo)數(shù)據(jù)源信息map集合,對(duì)其中的key,value進(jìn)行解析
this.targetDataSources.forEach((key, value) -> {
//resolveSpecifiedLookupKey方法沒(méi)有做任何處理,只是將key繼續(xù)返回
Object lookupKey = this.resolveSpecifiedLookupKey(key);
//將目標(biāo)數(shù)據(jù)源map集合中的value值(德魯伊數(shù)據(jù)源信息)轉(zhuǎn)為DataSource類(lèi)型
DataSource dataSource = this.resolveSpecifiedDataSource(value);
//將解析之后的key,value放入resolvedDataSources集合中
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
//將默認(rèn)目標(biāo)數(shù)據(jù)源信息解析并賦值給resolvedDefaultDataSource
this.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);
}
}
//因?yàn)锳bstractRoutingDataSource繼承AbstractDataSource,而AbstractDataSource實(shí)現(xiàn)了DataSource接口,所有存在獲取數(shù)據(jù)源連接的方法
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");
//調(diào)用實(shí)現(xiàn)類(lèi)中重寫(xiě)的determineCurrentLookupKey方法拿到當(dāng)前線程要使用的數(shù)據(jù)源的名稱(chēng)
Object lookupKey = this.determineCurrentLookupKey();
//去解析之后的數(shù)據(jù)源信息集合中查詢(xún)?cè)摂?shù)據(jù)源是否存在,如果沒(méi)有拿到則使用默認(rèn)數(shù)據(jù)源resolvedDefaultDataSource
DataSource 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;
}
}
@Nullable
protected abstract Object determineCurrentLookupKey();
}
2.2、DynamicDataSource類(lèi)
**
* @Description: TODO:動(dòng)態(tài)數(shù)據(jù)源
* @Author: yyalin
* @CreateDate: 2023/7/16 14:46
* @Version: V1.0
*/
/**
*
* 調(diào)用AddDefineDataSource組件的addDefineDynamicDataSource()方法,獲取原來(lái)targetdatasources的map,
* 并將新的數(shù)據(jù)源信息添加到map中,并替換targetdatasources中的map
* 切換數(shù)據(jù)源時(shí)可以使用@DataSource(value = "數(shù)據(jù)源名稱(chēng)"),或者DynamicDataSourceContextHolder.setContextKey("數(shù)據(jù)源名稱(chēng)")
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DynamicDataSource extends AbstractRoutingDataSource {
//備份所有數(shù)據(jù)源信息,
private Map<Object, Object> defineTargetDataSources;
/**
* 決定當(dāng)前線程使用哪個(gè)數(shù)據(jù)源
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDynamicDataSourceKey();
}
}
2.3、DynamicDataSourceHolder
**
* @Description: TODO:數(shù)據(jù)源切換處理
* DynamicDataSourceHolder類(lèi)主要是設(shè)置當(dāng)前線程的數(shù)據(jù)源名稱(chēng),
* 移除數(shù)據(jù)源名稱(chēng),以及獲取當(dāng)前數(shù)據(jù)源的名稱(chēng),便于動(dòng)態(tài)切換
* @Author: yyalin
* @CreateDate: 2023/7/16 14:51
* @Version: V1.0
*/
@Slf4j
public class DynamicDataSourceHolder {
/**
* 保存動(dòng)態(tài)數(shù)據(jù)源名稱(chēng)
*/
private static final ThreadLocal<String> DYNAMIC_DATASOURCE_KEY = new ThreadLocal<>();
/**
* 設(shè)置/切換數(shù)據(jù)源,決定當(dāng)前線程使用哪個(gè)數(shù)據(jù)源
*/
public static void setDynamicDataSourceKey(String key){
log.info("數(shù)據(jù)源切換為:{}",key);
DYNAMIC_DATASOURCE_KEY.set(key);
}
/**
* 獲取動(dòng)態(tài)數(shù)據(jù)源名稱(chēng),默認(rèn)使用mater數(shù)據(jù)源
*/
public static String getDynamicDataSourceKey(){
String key = DYNAMIC_DATASOURCE_KEY.get();
return key == null ? DbsConstant.MySQL_db_01 : key;
}
/**
* 移除當(dāng)前數(shù)據(jù)源
*/
public static void removeDynamicDataSourceKey(){
log.info("移除數(shù)據(jù)源:{}",DYNAMIC_DATASOURCE_KEY.get());
DYNAMIC_DATASOURCE_KEY.remove();
}
}
2.4、數(shù)據(jù)源工具類(lèi)
**
* @Description: TODO:數(shù)據(jù)源工具類(lèi)
* @Author: yyalin
* @CreateDate: 2023/7/16 15:00
* @Version: V1.0
*/
@Slf4j
@Component
public class DataSourceUtils {
@Resource
DynamicDataSource dynamicDataSource;
/**
* @Description: 根據(jù)傳遞的數(shù)據(jù)源信息測(cè)試數(shù)據(jù)庫(kù)連接
* @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("數(shù)據(jù)源連接成功");
return druidDataSource;
} catch (SQLException throwables) {
log.error("數(shù)據(jù)源 {} 連接失敗,用戶(hù)名:{},密碼 {}",dataSourceInfo.getUrl(),dataSourceInfo.getUserName(),dataSourceInfo.getPassword());
return null;
}
}
/**
* @Description: 將新增的數(shù)據(jù)源加入到備份數(shù)據(jù)源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:數(shù)據(jù)源信息配置類(lèi),讀取數(shù)據(jù)源配置信息并注冊(cè)成bean。
* @Author: yyalin
* @CreateDate: 2023/7/16 14:54
* @Version: V1.0
*/
@Configuration
@MApperScan("com.wonders.mapper")
@Slf4j
public class DynamicDataSourceConfig {
@Bean(name = DbsConstant.mysql_db_01)
@ConfigurationProperties("spring.datasource.mysqldb01")
public DataSource masterDataSource() {
log.info("數(shù)據(jù)源切換為:{}",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("數(shù)據(jù)源切換為:{}",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("數(shù)據(jù)源切換為oracle:{}",DbsConstant.oracle_db_01);
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return dataSource;
}
@Bean
@Primary
public 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());
//設(shè)置動(dòng)態(tài)數(shù)據(jù)源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
dynamicDataSource.setTargetDataSources(dataSourceMap);
//將數(shù)據(jù)源信息備份在defineTargetDataSources中
dynamicDataSource.setDefineTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
}
三、測(cè)試代碼
/**
* @Description: TODO
* @Author: yyalin
* @CreateDate: 2023/7/16 15:02
* @Version: V1.0
*/
@Slf4j
@Api(tags="動(dòng)態(tài)切換多數(shù)據(jù)源測(cè)試")
@RestController
public class TestController {
@Resource
DataSourceUtils dataSourceUtils;
@Autowired
private StudentMapper studentMapper;
@ApiOperation(value="動(dòng)態(tài)切換多數(shù)據(jù)源測(cè)試", notes="test")
@GetMapping("/test")
public Map<String, Object> dynamicDataSourceTest(String id){
Map<String, Object> map = new HashMap<>();
//1、默認(rèn)庫(kù)中查詢(xún)數(shù)據(jù)
Student student=studentMapper.selectById(id);
map.put("1、默認(rèn)庫(kù)中查詢(xún)到的數(shù)據(jù)",student);
//2、指定庫(kù)中查詢(xún)的數(shù)據(jù)
DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.mysql_db_02);
Student student02=studentMapper.selectById(id);
map.put("2、指定庫(kù)中查詢(xún)的數(shù)據(jù)",student02);
//3、從數(shù)據(jù)庫(kù)獲取連接信息,然后獲取數(shù)據(jù)
//模擬從數(shù)據(jù)庫(kù)中獲取的連接
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("數(shù)據(jù)源信息:{}",dataSourceInfo);
//測(cè)試數(shù)據(jù)源連接
DruidDataSource druidDataSource = dataSourceUtils.createDataSourceConnection(dataSourceInfo);
if (Objects.nonNull(druidDataSource)){
//將新的數(shù)據(jù)源連接添加到目標(biāo)數(shù)據(jù)源map中
dataSourceUtils.addDefineDynamicDataSource(druidDataSource,dataSourceInfo.getDatasourceKey());
//設(shè)置當(dāng)前線程數(shù)據(jù)源名稱(chēng)-----代碼形式
DynamicDataSourceHolder.setDynamicDataSourceKey(dataSourceInfo.getDatasourceKey());
//在新的數(shù)據(jù)源中查詢(xún)用戶(hù)信息
Student student03=studentMapper.selectById(id);
map.put("3、動(dòng)態(tài)數(shù)據(jù)源查詢(xún)的數(shù)據(jù)",student03);
//關(guān)閉數(shù)據(jù)源連接
druidDataSource.close();
}
//4、指定oracle庫(kù)中查詢(xún)的數(shù)據(jù)
DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.oracle_db_01);
Student student04=studentMapper.selectById(id);
map.put("4、指定oracle庫(kù)中查詢(xún)的數(shù)據(jù)",student04);
return map;
}
}
測(cè)試結(jié)果如下:
從結(jié)果中可以明顯的看出,通過(guò)切換不同的數(shù)據(jù)源,可以從不同的庫(kù)中獲取不同的數(shù)據(jù),包括:常見(jiàn)庫(kù)Mysql、oracle、sqlserver等數(shù)據(jù)庫(kù)相互切換。也可以從數(shù)據(jù)庫(kù)的某張表中獲取連接信息,實(shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)庫(kù)。
四、使用注解方式切換數(shù)據(jù)源
從上述TestController 中代碼不難看出,若要想切換數(shù)據(jù)源需要在mapper調(diào)用之前調(diào)用:
DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.mysql_db_02);
不夠簡(jiǎn)潔優(yōu)雅,所以下面推薦使用注解的方式來(lái)動(dòng)態(tài)進(jìn)行數(shù)據(jù)源的切換。
4.1、創(chuàng)建注解類(lèi)DataSource
/**
* @Description: TODO:自定義多數(shù)據(jù)源切換注解
* 優(yōu)先級(jí):先方法,后類(lèi),如果方法覆蓋了類(lèi)上的數(shù)據(jù)源類(lèi)型,以方法的為準(zhǔn),否則以類(lèi)上的為準(zhǔn)
* @Author: yyalin
* @CreateDate: 2023/7/17 14:00
* @Version: V1.0
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
//切換數(shù)據(jù)源名稱(chēng),默認(rèn)mysql_db_01
public String value() default DbsConstant.mysql_db_01;
}
4.2、創(chuàng)建切面DataSourceAspect類(lèi)
**
* @Description: TODO:創(chuàng)建切面DataSourceAspect類(lèi)
* @Author: yyalin
* @CreateDate: 2023/7/17 14:03
* @Version: V1.0
*/
@Aspect
@Component
public class DataSourceAspect {
// 設(shè)置DataSource注解的切點(diǎn)表達(dá)式
@Pointcut("@annotation(com.wonders.dynamic.DataSource)")
public void dynamicDataSourcePointCut(){}
//環(huán)繞通知
@Around("dynamicDataSourcePointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
String key = getDefineAnnotation(joinPoint).value();
DynamicDataSourceHolder.setDynamicDataSourceKey(key);
try {
return joinPoint.proceed();
} finally {
DynamicDataSourceHolder.removeDynamicDataSourceKey();
}
}
/**
* 功能描述:先判斷方法的注解,后判斷類(lèi)的注解,以方法的注解為準(zhǔn)
* @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、進(jìn)行數(shù)據(jù)源切換
/@Mapper 與 啟動(dòng)類(lèi)的@MapperScan({"com.example.demo.mapper"}) 二選一即可
@Repository
public interface StudentMapper extends BaseMapper<Student> {
/**
* 功能描述:在mysql_db_01中查詢(xún)數(shù)據(jù)
* @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層
Service
public class StudentServiceImpl implements StudentService{
@Autowired
private StudentMapper studentMapper;
//注解加在實(shí)現(xiàn)層才能生效
@DataSource(value = DbsConstant.mysql_db_01)
@Override
public Student findStudentById(String id) {
return studentMapper.selectById(id);
}
}
4.3、測(cè)試效果
ApiOperation(value="使用注解方式動(dòng)態(tài)切換多數(shù)據(jù)源", notes="test02")
@GetMapping("/test02")
public Student test02(String id){
Student student=studentMapper.findStudentById(id);
return student;
}
--結(jié)果如下: