相信在很多使用MybatisPlus框架的小伙伴都會遇到多數(shù)據(jù)源的配置問題泳秀,并且官網(wǎng)也給出了推薦使用多數(shù)據(jù)源 (dynamic-datasource-spring-boot-starter
) 組件來實現(xiàn)阅爽。由于最近項目也在使用這個組件來實現(xiàn)多數(shù)據(jù)源切換疯淫,因此想了解一下該組件是如何運行的韩脑,經(jīng)過自己的調(diào)試宝踪,簡單記錄一下這個組件的實現(xiàn),也以便日后組件如果出問題了或者某些地方需要開次開發(fā)時有個參考。
簡單實現(xiàn)數(shù)據(jù)源切換
數(shù)據(jù)庫demo
本例子使用的是同一個MYSQL服務(wù)躺枕,不同數(shù)據(jù)庫來進行調(diào)試的,具體如圖所示
SpringBoot demo
添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
配置YML文件
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 5
maximum-pool-size: 15
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
pool-name: OasisHikariCP
connection-test-query: SELECT 1
dynamic:
primary: db1 #默認(rèn)主數(shù)據(jù)源
datasource:
db1: #配置主數(shù)據(jù)源
url: jdbc:mysql://ip:3306/demo_user?useSSL=true&requireSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
db2: #配置其他數(shù)據(jù)源
url: jdbc:mysql://ip:3306/demo_class?useSSL=true&requireSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
MybaitsPlus
實體層
UserEntity.class
/**
* description: DB1中的實體
* date: 2021/7/13 13:38 <br>
* author: Neal <br>
* version: 1.0 <br>
*/
@TableName("user_t")
public class UserEntity {
private long id;
private String userName;
private String userSex;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserSex() {
return userSex;
}
public void setUserSex(String userSex) {
this.userSex = userSex;
}
}
ClassEntity.class
/**
* description: DB2中的實體
* date: 2021/7/13 13:40 <br>
* author: Neal <br>
* version: 1.0 <br>
*/
@TableName("class_t")
public class ClassEntity {
private String name;
private String number;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
}
mapper層
UserMapper.class(使用默認(rèn)數(shù)據(jù)源)
/**
* description: UserMapper <br>
* date: 2021/7/13 13:41 <br>
* author: Neal <br>
* version: 1.0 <br>
*/
public interface UserMapper extends BaseMapper<UserEntity> {
}
ClassMapper.class(使用另外一個數(shù)據(jù)源)
/**
* description: ClassMapper <br>
* date: 2021/7/13 13:41 <br>
* author: Neal <br>
* version: 1.0 <br>
*/
@DS("db2") //使用另外一個數(shù)據(jù)源
public interface ClassMapper extends BaseMapper<ClassEntity> {
}
單元測試
結(jié)果已經(jīng)是可以完美運行多數(shù)據(jù)源。
源碼解析
在我們搞項目中拐云,不僅要學(xué)會用這些組件罢猪,更重要的是 要知其所以然,知道他是如何實現(xiàn)的叉瘩,其實原理也就是網(wǎng)上能搜到的基于切面的代理處理方式膳帕,但是其中有些內(nèi)容還是值得去學(xué)習(xí)。
自動裝配
首先我們從 dynamic-datasource
組件的自動裝配開始
接下來讓我們來看一下 這個自動裝配類薇缅,所裝配的Bean
@Slf4j
@Configuration
//啟動SpringBoot 自動裝配 DynamicDataSourceProperties外部化配置
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
//聲明裝配加載順序,在 DataSourceAutoConfiguration 之前加載
@AutoConfigureBefore(value = DataSourceAutoConfiguration.class, name = "com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure")
//當(dāng)自動裝配時危彩,引入并自動裝配下列三個自動裝配類
@Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class, DynamicDataSourceHealthCheckConfiguration.class})
//自動裝配加載條件 當(dāng) spring.datasource.dynamic = true時 進行自動裝配的加載,默認(rèn)缺省為true
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceAutoConfiguration implements InitializingBean {
//注入外部化配置
private final DynamicDataSourceProperties properties;
private final List<DynamicDataSourcePropertiesCustomizer> dataSourcePropertiesCustomizers;
//構(gòu)造函數(shù)注入
public DynamicDataSourceAutoConfiguration(
DynamicDataSourceProperties properties,
ObjectProvider<List<DynamicDataSourcePropertiesCustomizer>> dataSourcePropertiesCustomizers) {
this.properties = properties;
this.dataSourcePropertiesCustomizers = dataSourcePropertiesCustomizers.getIfAvailable();
}
//多數(shù)據(jù)源加載接口泳桦,默認(rèn)的實現(xiàn)為從yml信息中加載所有數(shù)據(jù)源
@Bean
public DynamicDataSourceProvider ymlDynamicDataSourceProvider() {
return new YmlDynamicDataSourceProvider(properties.getDatasource());
}
//實現(xiàn)DataSource JAVA JNDI 后期Spring 容器中 所有的數(shù)據(jù)庫連接都從該實現(xiàn)Bean 中獲取
@Bean
@ConditionalOnMissingBean
public DataSource dataSource() {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setPrimary(properties.getPrimary());
dataSource.setStrict(properties.getStrict());
dataSource.setStrategy(properties.getStrategy());
dataSource.setP6spy(properties.getP6spy());
dataSource.setSeata(properties.getSeata());
return dataSource;
}
//設(shè)置動態(tài)數(shù)據(jù)源轉(zhuǎn)換切換配置器
@Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
@Bean
public Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(properties.isAllowedPublicOnly(), dsProcessor);
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
advisor.setOrder(properties.getOrder());
return advisor;
}
//數(shù)據(jù)庫事務(wù)的切面配置類
@Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "seata", havingValue = "false", matchIfMissing = true)
@Bean
public Advisor dynamicTransactionAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("@annotation(com.baomidou.dynamic.datasource.annotation.DSTransactional)");
return new DefaultPointcutAdvisor(pointcut, new DynamicLocalTransactionAdvisor());
}
//DynamicDataSourceAnnotationInterceptor 切面配置器中所需要的執(zhí)行鏈汤徽,
//主要用來確定使用哪個數(shù)據(jù)源
@Bean
@ConditionalOnMissingBean
public DsProcessor dsProcessor(BeanFactory beanFactory) {
DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
DsSessionProcessor sessionProcessor = new DsSessionProcessor();
DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory));
headerProcessor.setNextProcessor(sessionProcessor);
sessionProcessor.setNextProcessor(spelExpressionProcessor);
return headerProcessor;
}
//Bean注入后所執(zhí)行的方法,本Demo中目前暫無使用
@Override
public void afterPropertiesSet() {
if (!CollectionUtils.isEmpty(dataSourcePropertiesCustomizers)) {
for (DynamicDataSourcePropertiesCustomizer customizer : dataSourcePropertiesCustomizers) {
customizer.customize(properties);
}
}
}
}
大體上的自動裝配已經(jīng)介紹完了灸撰,接下來我們逐個將重要的代碼段或者類來進行解釋
DynamicDataSourceCreatorAutoConfiguration 分析
這個類主要是進行數(shù)據(jù)源加載的 主要代碼如下
@Slf4j
@Configuration
@AllArgsConstructor
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceCreatorAutoConfiguration {
//描述Bean的 注入順序
public static final int JNDI_ORDER = 1000;
public static final int DRUID_ORDER = 2000;
public static final int HIKARI_ORDER = 3000;
public static final int BEECP_ORDER = 4000;
public static final int DBCP2_ORDER = 5000;
public static final int DEFAULT_ORDER = 6000;
private final DynamicDataSourceProperties properties;
//默認(rèn)的數(shù)據(jù)源創(chuàng)造器
@Primary
@Bean
@ConditionalOnMissingBean
public DefaultDataSourceCreator dataSourceCreator(List<DataSourceCreator> dataSourceCreators) {
DefaultDataSourceCreator defaultDataSourceCreator = new DefaultDataSourceCreator();
defaultDataSourceCreator.setProperties(properties);
defaultDataSourceCreator.setCreators(dataSourceCreators);
return defaultDataSourceCreator;
}
//省略部分代碼
/**
* 存在Hikari數(shù)據(jù)源時, 加入創(chuàng)建器
*/
@ConditionalOnClass(HikariDataSource.class)
@Configuration
public class HikariDataSourceCreatorConfiguration {
@Bean
@Order(HIKARI_ORDER)
@ConditionalOnMissingBean
public HikariDataSourceCreator hikariDataSourceCreator() {
return new HikariDataSourceCreator(properties.getHikari());
}
}
//省略部分代碼
}
當(dāng)Spring 容器注入 DefaultDataSourceCreator 實例后 谒府,接下來就被 DynamicDataSourceProvider 這個類所使用。
DynamicDataSourceProvider 分析
@Slf4j
@AllArgsConstructor
public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider {
/**
* 所有數(shù)據(jù)源
*/
private final Map<String, DataSourceProperty> dataSourcePropertiesMap;
//通過構(gòu)造函數(shù)注入所有的 數(shù)據(jù)源 然后調(diào)用該父類方法創(chuàng)建數(shù)據(jù)源集合
@Override
public Map<String, DataSource> loadDataSources() {
return createDataSourceMap(dataSourcePropertiesMap);
}
}
@Slf4j
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {
//從Spring 容器中獲取注入好的 DefaultDataSourceCreator
@Autowired
private DefaultDataSourceCreator defaultDataSourceCreator;
//創(chuàng)建數(shù)據(jù)源集合
protected Map<String, DataSource> createDataSourceMap(
Map<String, DataSourceProperty> dataSourcePropertiesMap) {
Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
DataSourceProperty dataSourceProperty = item.getValue();
String poolName = dataSourceProperty.getPoolName();
if (poolName == null || "".equals(poolName)) {
poolName = item.getKey();
}
dataSourceProperty.setPoolName(poolName);
dataSourceMap.put(poolName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
}
return dataSourceMap;
}
}
DynamicDataSourceAnnotationAdvisor 分析
這個其實就是Spring AOP的切面配置器 主要代碼如下
public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
//切面增強方法
private final Advice advice;
private final Pointcut pointcut;
//構(gòu)造方法注入
public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) {
this.advice = dynamicDataSourceAnnotationInterceptor;
this.pointcut = buildPointcut();
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
@Override
public Advice getAdvice() {
return this.advice;
}
//省略部分代碼
//當(dāng)有類或者方法中有 DS.class 注解時 進行 切面增強
private Pointcut buildPointcut() {
Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);
Pointcut mpc = new AnnotationMethodPoint(DS.class);
return new ComposablePointcut(cpc).union(mpc);
}
//省略部分代碼
}
DynamicDataSourceAnnotationInterceptor 分析
該類為切面增強浮毯,即當(dāng)上面的DynamicDataSourceAnnotationAdvisor 攔截到類或者方法中有 DS.class 注解時 完疫,調(diào)用該增強類進行處理
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {
/**
* The identification of SPEL.
*/
private static final String DYNAMIC_PREFIX = "#";
private final DataSourceClassResolver dataSourceClassResolver;
private final DsProcessor dsProcessor;
public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {
dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly);
this.dsProcessor = dsProcessor;
}
//AOP攔截后進行 切面增強方法
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//選擇數(shù)據(jù)源
String dsKey = determineDatasourceKey(invocation);
//使用基于ThreadLocal的實現(xiàn)切換數(shù)據(jù)源
DynamicDataSourceContextHolder.push(dsKey);
try {
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
//通過調(diào)用 DsProcessor 來鏈?zhǔn)秸{(diào)用進行 數(shù)據(jù)源的確認(rèn)
private String determineDatasourceKey(MethodInvocation invocation) {
String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
}
}
DefaultPointcutAdvisor 分析
該切面增強為事務(wù)增強,設(shè)置此增強類后债蓝,不能與Spring 源事務(wù)或者 @Transactional
注解共用壳鹤。
@Slf4j
public class DynamicLocalTransactionAdvisor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
if (!StringUtils.isEmpty(TransactionContext.getXID())) {
return methodInvocation.proceed();
}
boolean state = true;
Object o;
String xid = UUID.randomUUID().toString();
TransactionContext.bind(xid);
try {
o = methodInvocation.proceed();
} catch (Exception e) {
state = false;
throw e;
} finally {
ConnectionFactory.notify(state);
TransactionContext.remove();
}
return o;
}
}
DynamicDataSourceContextHolder 核心切換類
public final class DynamicDataSourceContextHolder {
/**
* 為什么要用鏈表存儲(準(zhǔn)確的是棧)
* <pre>
* 為了支持嵌套切換,如ABC三個service都是不同的數(shù)據(jù)源
* 其中A的某個業(yè)務(wù)要調(diào)B的方法惦蚊,B的方法需要調(diào)用C的方法。一級一級調(diào)用切換讯嫂,形成了鏈蹦锋。
* 傳統(tǒng)的只設(shè)置當(dāng)前線程的方式不能滿足此業(yè)務(wù)需求,必須使用棧欧芽,后進先出莉掂。
* </pre>
*/
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
@Override
protected Deque<String> initialValue() {
return new ArrayDeque<>();
}
};
private DynamicDataSourceContextHolder() {
}
/**
* 獲得當(dāng)前線程數(shù)據(jù)源
*
* @return 數(shù)據(jù)源名稱
*/
public static String peek() {
return LOOKUP_KEY_HOLDER.get().peek();
}
/**
* 設(shè)置當(dāng)前線程數(shù)據(jù)源
* <p>
* 如非必要不要手動調(diào)用,調(diào)用后確保最終清除
* </p>
*
* @param ds 數(shù)據(jù)源名稱
*/
public static String push(String ds) {
String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;
LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
return dataSourceStr;
}
/**
* 清空當(dāng)前線程數(shù)據(jù)源
* <p>
* 如果當(dāng)前線程是連續(xù)切換數(shù)據(jù)源 只會移除掉當(dāng)前線程的數(shù)據(jù)源名稱
* </p>
*/
public static void poll() {
Deque<String> deque = LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
/**
* 強制清空本地線程
* <p>
* 防止內(nèi)存泄漏千扔,如手動調(diào)用了push可調(diào)用此方法確保清除
* </p>
*/
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
}
大致核心的代碼已經(jīng)介紹完了憎妙,接下來我們逐步debugger,摸清其執(zhí)行流程。
數(shù)據(jù)源切換執(zhí)行流程
現(xiàn)在當(dāng)我們執(zhí)行上面的SpringBoot demo中的 調(diào)用注解 @DS("db2")
的 Mapper 查詢數(shù)據(jù)庫時曲楚,他的順序如下(只給出涉及到該組件的相關(guān)類)
ClassMapper#selectList()
: 執(zhí)行Mybatis查詢操作DynamicDataSourceAnnotationInterceptor#invoke()
: Spring AOP 攔截到帶有@DS("db2")
并執(zhí)行代理增強操作DataSourceClassResolver#findDSKey()
: 查找有注解@DS()
的 類或方法厘唾,獲取對應(yīng)的數(shù)據(jù)源Key 值 也就是 db2。DynamicDataSourceContextHolder#push()
: 設(shè)置當(dāng)前線程數(shù)據(jù)源-
DynamicRoutingDataSource#getConnection()
: 調(diào)用父類方法獲取數(shù)據(jù)庫連接 這里兩種處理方式 如下所示public Connection getConnection() throws SQLException { String xid = TransactionContext.getXID(); //無事務(wù)時 即當(dāng)前操作為 查詢 if (StringUtils.isEmpty(xid)) { return determineDataSource().getConnection(); } else { //有事物時 龙誊,先從 之前DynamicDataSourceContextHolder 中獲取數(shù)據(jù)源 先進先出原則 String ds = DynamicDataSourceContextHolder.peek(); ds = StringUtils.isEmpty(ds) ? "default" : ds; ConnectionProxy connection = ConnectionFactory.getConnection(ds); //創(chuàng)建數(shù)據(jù)源 return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection; } }
-
DynamicRoutingDataSource#getDataSource
設(shè)置數(shù)據(jù)源public DataSource getDataSource(String ds) { //如果當(dāng)前無 數(shù)據(jù)源聲明 則使用默認(rèn)數(shù)據(jù)源 if (StringUtils.isEmpty(ds)) { return determinePrimaryDataSource(); } else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) { log.debug("dynamic-datasource switch to the datasource named [{}]", ds); return groupDataSources.get(ds).determineDataSource(); } else if (dataSourceMap.containsKey(ds)) { //如果當(dāng)前存在數(shù)據(jù)源則取出該數(shù)據(jù)源返回 log.debug("dynamic-datasource switch to the datasource named [{}]", ds); return dataSourceMap.get(ds); } if (strict) { throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds); } return determinePrimaryDataSource(); }
- 執(zhí)行剩余的數(shù)據(jù)庫操作至結(jié)束抚垃。
小結(jié)
大體上寫的略微混亂,但是只要我們知道其自動裝配時 ,實例化了哪些Bean,并且知道這些Bean 是干什么的 鹤树,合適調(diào)用的铣焊,根據(jù)執(zhí)行流程逐步Debugger調(diào)試,就可以明白dynamic-datasource
組件是如何進行數(shù)據(jù)源切換的罕伯,在流程中我認(rèn)為比較經(jīng)典也是比較核心的地方已經(jīng)標(biāo)注出源碼曲伊。我們可以借鑒 DynamicDataSourceContextHolder
這個公共類的思想,擴展和優(yōu)化我們現(xiàn)有的項目中某些跨資源調(diào)用的問題追他。