一文讀懂Spring動態配置多資料來源---原始碼詳細分析
Spring動態多資料來源原始碼分析及解讀
一、為什麼要研究Spring動態多資料來源
期初,最開始的原因是:想將答題服務中傳送主觀題答題資料給批改中介軟體這塊抽象出來, 但這塊主要使用的是mq訊息的方式傳送到批改中介軟體,所以,最後決定將mq進行抽象,抽象後的結果是:語文,英語,通用任務都能個性化的配置mq,且可以擴充套件到任何使用mq的業務場景上。終端需要做的就是增加mq配置,自定義消費者業務邏輯方法,呼叫send方法即可。
這樣做的好處是:原本在每個使用到mq的專案裡都要寫一遍mq生產者,mq消費者,傳送mq資料,監聽mq消費等動作,且如果一個專案裡有多個mq配置,要寫多遍這樣的配置。抽象後,只需要配置檔案中進行配置,然後自定義個性化的業務邏輯消費者,就可以進行mq傳送了。
這樣一個可動態配置的mq,要求還是挺多的,如何動態配置? 如何能夠在伺服器啟動的時候就啟動n個mq的生產者和消費者? 傳送資料的時候, 怎麼找到正確的mq傳送呢?
其實, 我一直相信, 我遇到的問題, 肯定有大神已經遇到過, 並且已經有了成熟的解決方案了. 於是, 開始搜尋行業內的解決方案, 找了很久也沒找到,最後在同事的提示下,發現Spring動態配置多資料來源的思想和我想實現的動態配置多MQ的思想類似。於是,我開始花時間研究Spring動態多資料來源的原始碼。
二、Spring動態多資料來源框架梳理
2.1 框架結構
Spring動態多資料來源是一個我們在專案中常用到的元件,尤其是做專案重構,有多種資料庫,不同的請求可能會呼叫不同的資料來源。這時,就需要動態呼叫指定的資料來源。我們來看看Spring動態多資料來源的整體框架
上圖中虛線框部分是Spring動態多資料來源的幾個組成部分
- ds處理器
- aop切面
- 建立資料來源
- 動態資料來源提供者
- 動態連線資料庫
除此之外,還可以看到如下資訊:
- Spring動態多資料來源是通過動態配置配置檔案的方式來指定多資料來源的。
- Spring動態多資料來源支援四種類型的資料:base資料來源,jndi資料來源,druid資料來源,hikari資料來源。
- 多種觸發機制:通過header配置ds,通過session配置ds,通過spel配置ds,其中ds是datasource的簡稱。
- 支援資料來源巢狀:一個請求過來,這個請求可能會訪問多個數據源,也就是方法巢狀的時候呼叫多資料來源,也是支援的。
2.2 原始碼結構
Spring動態多資料來源的幾個組成部分,在程式碼原始碼結構中完美的體現出來。
上圖是Spring動態多資料來源的原始碼專案結構,我們主要列一下主要的結構
----annotation:定義了DS主機
----aop:定義了一個前置通知,切面類
----creator:動態多資料來源的建立器
----exception:異常處理
----matcher:匹配器
----processor:ds處理器
----provider:資料員提供者
----spring:spring動態多資料來源啟動配置相關類
----toolkit:工具包
----AbstractRoutingDataSource:動態路由資料來源抽象類
----DynamicRoutingDataSource:動態路由資料來源實現類
2.3 整體專案結構圖
下圖是Spring多型多資料來源的程式碼專案結構圖。
這個圖內容比較多,所以字比較小,大概看出一共有6個部分就可以了。後面會就每一個部分詳細說明。
三、專案原始碼分析
3.1 引入Spring依賴jar包.
Spring動態多資料來源,我們在使用的時候,直接引入jar,然後配置資料來源就可以使用了。配置jar包如下
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.1.1</version>
</dependency>
然後是在yml配置檔案中增加配置
# master
spring.datasource.dynamic.datasource.master.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.dynamic.datasource.master.url=jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.dynamic.datasource.master.username=root
spring.datasource.dynamic.datasource.master.password=123456
# slave
spring.datasource.dynamic.datasource.slave.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.dynamic.datasource.slave.url=jdbc:mysql://localhost:3306/test1?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.dynamic.datasource.slave.username=root
spring.datasource.dynamic.datasource.slave.password=123456
在測試的時候, 使用了兩個不同的資料庫, 一個是test,一個是test1
3.2 Spring 原始碼分析的入口
為什麼引入jar就能在專案裡使用了呢?因為在jar包裡配置了META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration
在這個檔案裡,指定了spring動態載入的時候要自動掃描的檔案DynamicDataSourceAutoConfiguration,這個檔案就是原始碼專案的入口了。這裡定義了專案啟動自動裝備DynamicDataSourceAutoConfiguration檔案。
接下來,我們就來看看DynamicDataSourceAutoConfiguration檔案。
3.3、Spring配置檔案入口。
下圖是DynamicDataSourceAutoConfiguration檔案的主要內容。
Spring配置檔案主要的作用是在系統載入的時候,就載入相關的bean。這裡專案初始化的時候都載入了哪些bean呢?
- 動態資料來源屬性類DynamicDataSourceProperties
- 資料來源處理器DsProcessor,採用責任鏈設計模式3種方法載入ds
- 動態資料來源註解類DynamicDataSourceAnnotationAdvisor,包括前置通知,切面類,切點的載入
- 資料來源建立器DataSourceCreator,這個方法是在另一個類被載入的DynamicDataSourceCreatorAutoConfiguration。也是自動配置bean類。可以選擇4種類型的資料來源進行建立。
- 資料來源提供者Provider,這是動態初始化資料來源,讀取yml配置檔案,在配置檔案中可配置1個或多個數據源。
接下來看一下原始碼
1. DynamicDataSourceAutoConfiguration動態資料來源配置檔案
@Slf4j
@Configuration
@AllArgsConstructor
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class})
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceAutoConfiguration {
private final DynamicDataSourceProperties properties;
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceProvider dynamicDataSourceProvider() {
Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
return new YmlDynamicDataSourceProvider(datasourceMap);
}
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setPrimary(properties.getPrimary());
dataSource.setStrict(properties.getStrict());
dataSource.setStrategy(properties.getStrategy());
dataSource.setProvider(dynamicDataSourceProvider);
dataSource.setP6spy(properties.getP6spy());
dataSource.setSeata(properties.getSeata());
return dataSource;
}
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceAnnotationAdvisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor();
interceptor.setDsProcessor(dsProcessor);
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
advisor.setOrder(properties.getOrder());
return advisor;
}
@Bean
@ConditionalOnMissingBean
public DsProcessor dsProcessor() {
DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
DsSessionProcessor sessionProcessor = new DsSessionProcessor();
DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
headerProcessor.setNextProcessor(sessionProcessor);
sessionProcessor.setNextProcessor(spelExpressionProcessor);
return headerProcessor;
}
@Bean
@ConditionalOnBean(DynamicDataSourceConfigure.class)
public DynamicDataSourceAdvisor dynamicAdvisor(DynamicDataSourceConfigure dynamicDataSourceConfigure, DsProcessor dsProcessor) {
DynamicDataSourceAdvisor advisor = new DynamicDataSourceAdvisor(dynamicDataSourceConfigure.getMatchers());
advisor.setDsProcessor(dsProcessor);
advisor.setOrder(Ordered.HIGHEST_PRECEDENCE);
return advisor;
}
}
看到這段程式碼,我們就比較熟悉了,這就是通過註解的方式,在專案啟動的時候,自動注入bean。我們來詳細看一下,他都注入了哪些內容。
- 動態多資料來源預置處理器dsProcess,ds就是datasource的簡稱。這裡主要採用的是責任鏈設計模式,獲取ds。
- 動態多資料來源註解通知dynamicDatasourceAnnotationAdvisor,這是一個aop前置通知,當一個請求發生的時候,會觸發前置通知,用來確定到底使用哪一個mq訊息佇列
- 動態多資料來源提供者dynamicDataSourceProvider,我們是動態配置多個數據源,那麼就有一個解析配置的過程,解析配置就是在這裡完成的,解析出多個數據源,然後分別呼叫資料來源建立者去建立資料來源。Spring動態多資料來源支援資料來源的巢狀。
- 動態路由到資料來源DynamicRoutingDataSource,當請求過來的時候,也找到對應的資料來源了,要建立資料庫連線,資料庫連線的操作就是在這裡完成的。
我們發現在這裡就有四個bean的初始化,並沒有bean的create建立過程,bean的建立過程是在另一個配置類(DynamicDataSourceCreatorAutoConfiguration)中完成的。
@Slf4j
@Configuration
@AllArgsConstructor
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceCreatorAutoConfiguration {
private final DynamicDataSourceProperties properties;
@Bean
@ConditionalOnMissingBean
public DataSourceCreator dataSourceCreator() {
DataSourceCreator dataSourceCreator = new DataSourceCreator();
dataSourceCreator.setBasicDataSourceCreator(basicDataSourceCreator());
dataSourceCreator.setJndiDataSourceCreator(jndiDataSourceCreator());
dataSourceCreator.setDruidDataSourceCreator(druidDataSourceCreator());
dataSourceCreator.setHikariDataSourceCreator(hikariDataSourceCreator());
dataSourceCreator.setGlobalPublicKey(properties.getPublicKey());
return dataSourceCreator;
}
@Bean
@ConditionalOnMissingBean
public BasicDataSourceCreator basicDataSourceCreator() {
return new BasicDataSourceCreator();
}
@Bean
@ConditionalOnMissingBean
public JndiDataSourceCreator jndiDataSourceCreator() {
return new JndiDataSourceCreator();
}
@Bean
@ConditionalOnMissingBean
public DruidDataSourceCreator druidDataSourceCreator() {
return new DruidDataSourceCreator(properties.getDruid());
}
@Bean
@ConditionalOnMissingBean
public HikariDataSourceCreator hikariDataSourceCreator() {
return new HikariDataSourceCreator(properties.getHikari());
}
}
大概是因為考慮到資料的種類比較多,所以將其單獨放到了一個配置裡面。從上面的原始碼可以看出,有四種類型的資料來源配置。分別是:basic、jndi、druid、hikari。這四種資料來源通過組合設計模式被set到DataSourceCreator中。
接下來,分別來看每一個模組都做了哪些事情。
四、通過責任鏈設計模式獲取資料來源名稱
Spring動態多資料來源, 獲取資料來源名稱的方式有3種,這3中方式採用的是責任鏈方式連續獲取的。首先在header中獲取,header中沒有,去session中獲取, session中也沒有, 通過spel獲取。
上圖是DSProcessor處理器的類圖。 一個介面量, 三個具體實現類,主要來看一下介面類實現
1. DsProcessor 抽象類
package com.baomidou.dynamic.datasource.processor;
import org.aopalliance.intercept.MethodInvocation;
public abstract class DsProcessor {
private DsProcessor nextProcessor;
public void setNextProcessor(DsProcessor dsProcessor) {
this.nextProcessor = dsProcessor;
}
/**
* 抽象匹配條件 匹配才會走當前執行器否則走下一級執行器
*
* @param key DS註解裡的內容
* @return 是否匹配
*/
public abstract boolean matches(String key);
/**
* 決定資料來源
* <pre>
* 呼叫底層doDetermineDatasource,
* 如果返回的是null則繼續執行下一個,否則直接返回
* </pre>
*
* @param invocation 方法執行資訊
* @param key DS註解裡的內容
* @return 資料來源名稱
*/
public String determineDatasource(MethodInvocation invocation, String key) {
if (matches(key)) {
String datasource = doDetermineDatasource(invocation, key);
if (datasource == null && nextProcessor != null) {
return nextProcessor.determineDatasource(invocation, key);
}
return datasource;
}
if (nextProcessor != null) {
return nextProcessor.determineDatasource(invocation, key);
}
return null;
}
/**
* 抽象最終決定資料來源
*
* @param invocation 方法執行資訊
* @param key DS註解裡的內容
* @return 資料來源名稱
*/
public abstract String doDetermineDatasource(MethodInvocation invocation, String key);
}
這裡定義了DsProcessor nextProcessor屬性, 下一個處理器。 判斷是否獲取到了datasource, 如果獲取到了則直接返回, 沒有獲取到,則呼叫下一個處理器。這個邏輯就是處理器的主邏輯,在determineDatasource(MethodInvocation invocation, String key)方法中實現。
接下來,每一個子類都會自定義實現doDetermineDatasource獲取目標資料來源的方法。不同的實現類獲取資料來源的方式是不同的。
下面看看具體實現類的主邏輯程式碼
2.DsHeaderProcessor: 從請求的header中獲取ds資料來源名稱。
public class DsHeaderProcessor extends DsProcessor {
/**
* header prefix
*/
private static final String HEADER_PREFIX = "#header";
@Override
public boolean matches(String key) {
return key.startsWith(HEADER_PREFIX);
}
@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getHeader(key.substring(8));
}
}
3.DsSessionProcessor: 從session中獲取資料來源d名稱
public class DsSessionProcessor extends DsProcessor {
/**
* session開頭
*/
private static final String SESSION_PREFIX = "#session";
@Override
public boolean matches(String key) {
return key.startsWith(SESSION_PREFIX);
}
@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getSession().getAttribute(key.substring(9)).toString();
}
}
4. DsSpelExpressionProcessor: 通過spel表示式獲取ds資料來源名稱
public class DsSpelExpressionProcessor extends DsProcessor {
/**
* 引數發現器
*/
private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
/**
* Express語法解析器
*/
private static final ExpressionParser PARSER = new SpelExpressionParser();
/**
* 解析上下文的模板
* 對於預設不設定的情況下,從引數中取值的方式 #param1
* 設定指定模板 ParserContext.TEMPLATE_EXPRESSION 後的取值方式: #{#param1}
* issues: https://github.com/baomidou/dynamic-datasource-spring-boot-starter/issues/199
*/
private ParserContext parserContext = new ParserContext() {
@Override
public boolean isTemplate() {
return false;
}
@Override
public String getExpressionPrefix() {
return null;
}
@Override
public String getExpressionSuffix() {
return null;
}
};
@Override
public boolean matches(String key) {
return true;
}
@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
Method method = invocation.getMethod();
Object[] arguments = invocation.getArguments();
EvaluationContext context = new MethodBasedEvaluationContext(null, method, arguments, NAME_DISCOVERER);
final Object value = PARSER.parseExpression(key, parserContext).getValue(context);
return value == null ? null : value.toString();
}
public void setParserContext(ParserContext parserContext) {
this.parserContext = parserContext;
}
}
他們三個的層級關係是在哪裡定義的呢?在DynamicDataSourceAutoConfiguration.java配置檔案中
5. DynamicDataSourceAutoConfiguration.java配置檔案
@Bean
@ConditionalOnMissingBean
public DsProcessor dsProcessor() {
DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
DsSessionProcessor sessionProcessor = new DsSessionProcessor();
DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
headerProcessor.setNextProcessor(sessionProcessor);
sessionProcessor.setNextProcessor(spelExpressionProcessor);
return headerProcessor;
}
第一層是headerProcessor,第二層是sessionProcessor, 第三層是spelExpressionProcessor。層級呼叫,最後獲得ds。
以上就是對資料來源處理器模組的的分析,那麼最終在哪裡被呼叫呢?來看下一個模組。
五、動態資料來源註解通知模組
這一塊對應的原始碼結構如下:
這個模組裡主要有三部分:
- 切面類:DynamicDataSourceAdvisor,DynamicDataSourceAnnotationAdvisor
- 切點類:DynamicAspectJExpressionPointcut,DynamicJdkRegexpMethodPointcut
- 前置通知類:DynamicDataSourceAnnotationInterceptor
他們之間的關係如下。這裡主要是aop方面的知識體系。具體專案結構圖如下:
因為在專案中使用最多的情況是通過註解的方式來解析,所以,我們重點看一下兩個檔案
1.DynamicDataSourceAnnotationInterceptor:自定義的前置通知類
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {
/**
* The identification of SPEL.
*/
private static final String DYNAMIC_PREFIX = "#";
private static final DataSourceClassResolver RESOLVER = new DataSourceClassResolver();
@Setter
private DsProcessor dsProcessor;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
DynamicDataSourceContextHolder.push(determineDatasource(invocation));
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
private String determineDatasource(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
DS ds = method.isAnnotationPresent(DS.class) ? method.getAnnotation(DS.class)
: AnnotationUtils.findAnnotation(RESOLVER.targetClass(invocation), DS.class);
String key = ds.value();
return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
}
}
這裡入參中有一個是DsProcessor,也就是ds處理器。在determineDatasource中看看DS的value值是否包含#,如果包含就經過dsProcessor處理後獲得key,如果不包含#則直接返回註解的value值。
2.DynamicDataSourceAnnotationAdvisor 切面類
public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements
BeanFactoryAware {
private Advice advice;
private Pointcut pointcut;
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;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (this.advice instanceof BeanFactoryAware) {
((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
}
}
private Pointcut buildPointcut() {
Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);
Pointcut mpc = AnnotationMatchingPointcut.forMethodAnnotation(DS.class);
return new ComposablePointcut(cpc).union(mpc);
}
}
在切面類的建構函式中設定了前置通知和切點。這個類在專案啟動的時候就會被載入。所有帶有DS註解的方法都會被掃描,在方法被呼叫的時候觸發前置通知。
六、資料來源建立器
這是最底層的操作了,建立資料來源。至於到底建立哪種型別的資料來源,是由上層配置決定的,在這裡,定義了4中型別的資料來源。 並通過組合的方式,用到那個資料來源,就動態的建立哪個資料來源。
下面來看這個模組的原始碼結構:
這裡面定義了一個數據源組合類和四種類型的資料來源。我們來看看他們之間的關係
四個基本的資料來源類,最後通過DataSourceCreator類組合建立資料來源,這裡面使用了簡單工廠模式建立類。下面來一個一個看看
1.BasicDataSourceCreator:基礎資料來源建立器
package com.baomidou.dynamic.datasource.creator;
import com.baomidou.dynamic.datasource.exception.ErrorCreateDataSourceException;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import javax.sql.DataSource;
import java.lang.reflect.Method;
/**
* 基礎資料來源建立器
*
* @author TaoYu
* @since 2020/1/21
*/
@Data
@Slf4j
public class BasicDataSourceCreator {
private static Method createMethod;
private static Method typeMethod;
private static Method urlMethod;
private static Method usernameMethod;
private static Method passwordMethod;
private static Method driverClassNameMethod;
private static Method buildMethod;
static {
//to support springboot 1.5 and 2.x
Class<?> builderClass = null;
try {
builderClass = Class.forName("org.springframework.boot.jdbc.DataSourceBuilder");
} catch (Exception ignored) {
}
if (builderClass == null) {
try {
builderClass = Class.forName("org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder");
} catch (Exception e) {
log.warn("not in springBoot ENV,could not create BasicDataSourceCreator");
}
}
if (builderClass != null) {
try {
createMethod = builderClass.getDeclaredMethod("create");
typeMethod = builderClass.getDeclaredMethod("type", Class.class);
urlMethod = builderClass.getDeclaredMethod("url", String.class);
usernameMethod = builderClass.getDeclaredMethod("username", String.class);
passwordMethod = builderClass.getDeclaredMethod("password", String.class);
driverClassNameMethod = builderClass.getDeclaredMethod("driverClassName", String.class);
buildMethod = builderClass.getDeclaredMethod("build");
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 建立基礎資料來源
*
* @param dataSourceProperty 資料來源引數
* @return 資料來源
*/
public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
try {
Object o1 = createMethod.invoke(null);
Object o2 = typeMethod.invoke(o1, dataSourceProperty.getType());
Object o3 = urlMethod.invoke(o2, dataSourceProperty.getUrl());
Object o4 = usernameMethod.invoke(o3, dataSourceProperty.getUsername());
Object o5 = passwordMethod.invoke(o4, dataSourceProperty.getPassword());
Object o6 = driverClassNameMethod.invoke(o5, dataSourceProperty.getDriverClassName());
return (DataSource) buildMethod.invoke(o6);
} catch (Exception e) {
throw new ErrorCreateDataSourceException(
"dynamic-datasource create basic database named " + dataSourceProperty.getPoolName() + " error");
}
}
}
這裡就有兩塊,一個是類初始化的時候初始化成員變數, 另一個是建立資料來源。當被呼叫createDataSource的時候執行建立資料來源,使用的反射機制建立資料來源。
2.JndiDataSourceCreator 使用jndi的方式建立資料來源
public class JndiDataSourceCreator {
private static final JndiDataSourceLookup LOOKUP = new JndiDataSourceLookup();
/**
* 建立基礎資料來源
*
* @param name 資料來源引數
* @return 資料來源
*/
public DataSource createDataSource(String name) {
return LOOKUP.getDataSource(name);
}
}
這裡通過name查詢的方式過去datasource
3.DruidDataSourceCreator: 建立druid型別的資料來源
public class DruidDataSourceCreator {
private DruidConfig druidConfig;
@Autowired(required = false)
private ApplicationContext applicationContext;
public DruidDataSourceCreator(DruidConfig druidConfig) {
this.druidConfig = druidConfig;
}
public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername(dataSourceProperty.getUsername());
dataSource.setPassword(dataSourceProperty.getPassword());
dataSource.setUrl(dataSourceProperty.getUrl());
dataSource.setDriverClassName(dataSourceProperty.getDriverClassName());
dataSource.setName(dataSourceProperty.getPoolName());
DruidConfig config = dataSourceProperty.getDruid();
Properties properties = config.toProperties(druidConfig);
String filters = properties.getProperty("druid.filters");
List<Filter> proxyFilters = new ArrayList<>(2);
if (!StringUtils.isEmpty(filters) && filters.contains("stat")) {
StatFilter statFilter = new StatFilter();
statFilter.configFromProperties(properties);
proxyFilters.add(statFilter);
}
if (!StringUtils.isEmpty(filters) && filters.contains("wall")) {
WallConfig wallConfig = DruidWallConfigUtil.toWallConfig(dataSourceProperty.getDruid().getWall(), druidConfig.getWall());
WallFilter wallFilter = new WallFilter();
wallFilter.setConfig(wallConfig);
proxyFilters.add(wallFilter);
}
if (!StringUtils.isEmpty(filters) && filters.contains("slf4j")) {
Slf4jLogFilter slf4jLogFilter = new Slf4jLogFilter();
// 由於properties上面被用了,LogFilter不能使用configFromProperties方法,這裡只能一個個set了。
DruidSlf4jConfig slf4jConfig = druidConfig.getSlf4j();
slf4jLogFilter.setStatementLogEnabled(slf4jConfig.getEnable());
slf4jLogFilter.setStatementExecutableSqlLogEnable(slf4jConfig.getStatementExecutableSqlLogEnable());
proxyFilters.add(slf4jLogFilter);
}
if (this.applicationContext != null) {
for (String filterId : druidConfig.getProxyFilters()) {
proxyFilters.add(this.applicationContext.getBean(filterId, Filter.class));
}
}
dataSource.setProxyFilters(proxyFilters);
dataSource.configFromPropety(properties);
//連線引數單獨設定
dataSource.setConnectProperties(config.getConnectionProperties());
//設定druid內建properties不支援的的引數
Boolean testOnReturn = config.getTestOnReturn() == null ? druidConfig.getTestOnReturn() : config.getTestOnReturn();
if (testOnReturn != null && testOnReturn.equals(true)) {
dataSource.setTestOnReturn(true);
}
Integer validationQueryTimeout =
config.getValidationQueryTimeout() == null ? druidConfig.getValidationQueryTimeout() : config.getValidationQueryTimeout();
if (validationQueryTimeout != null && !validationQueryTimeout.equals(-1)) {
dataSource.setValidationQueryTimeout(validationQueryTimeout);
}
Boolean sharePreparedStatements =
config.getSharePreparedStatements() == null ? druidConfig.getSharePreparedStatements() : config.getSharePreparedStatements();
if (sharePreparedStatements != null && sharePreparedStatements.equals(true)) {
dataSource.setSharePreparedStatements(true);
}
Integer connectionErrorRetryAttempts =
config.getConnectionErrorRetryAttempts() == null ? druidConfig.getConnectionErrorRetryAttempts()
: config.getConnectionErrorRetryAttempts();
if (connectionErrorRetryAttempts != null && !connectionErrorRetryAttempts.equals(1)) {
dataSource.setConnectionErrorRetryAttempts(connectionErrorRetryAttempts);
}
Boolean breakAfterAcquireFailure =
config.getBreakAfterAcquireFailure() == null ? druidConfig.getBreakAfterAcquireFailure() : config.getBreakAfterAcquireFailure();
if (breakAfterAcquireFailure != null && breakAfterAcquireFailure.equals(true)) {
dataSource.setBreakAfterAcquireFailure(true);
}
Integer timeout = config.getRemoveAbandonedTimeoutMillis() == null ? druidConfig.getRemoveAbandonedTimeoutMillis()
: config.getRemoveAbandonedTimeoutMillis();
if (timeout != null) {
dataSource.setRemoveAbandonedTimeout(timeout);
}
Boolean abandoned = config.getRemoveAbandoned() == null ? druidConfig.getRemoveAbandoned() : config.getRemoveAbandoned();
if (abandoned != null) {
dataSource.setRemoveAbandoned(abandoned);
}
Boolean logAbandoned = config.getLogAbandoned() == null ? druidConfig.getLogAbandoned() : config.getLogAbandoned();
if (logAbandoned != null) {
dataSource.setLogAbandoned(logAbandoned);
}
Integer queryTimeOut = config.getQueryTimeout() == null ? druidConfig.getQueryTimeout() : config.getQueryTimeout();
if (queryTimeOut != null) {
dataSource.setQueryTimeout(queryTimeOut);
}
Integer transactionQueryTimeout =
config.getTransactionQueryTimeout() == null ? druidConfig.getTransactionQueryTimeout() : config.getTransactionQueryTimeout();
if (transactionQueryTimeout != null) {
dataSource.setTransactionQueryTimeout(transactionQueryTimeout);
}
try {
dataSource.init();
} catch (SQLException e) {
throw new ErrorCreateDataSourceException("druid create error", e);
}
return dataSource;
}
}
其實,這裡面重點方法也是createDataSource(), 如果看不太明白是怎麼建立的,一點關係都沒有,就知道通過這種方式建立了資料來源就ok了。
4. HikariDataSourceCreator: 建立Hikari型別的資料來源
@Data
@AllArgsConstructor
public class HikariDataSourceCreator {
private HikariCpConfig hikariCpConfig;
public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
HikariConfig config = dataSourceProperty.getHikari().toHikariConfig(hikariCpConfig);
config.setUsername(dataSourceProperty.getUsername());
config.setPassword(dataSourceProperty.getPassword());
config.setJdbcUrl(dataSourceProperty.getUrl());
config.setDriverClassName(dataSourceProperty.getDriverClassName());
config.setPoolName(dataSourceProperty.getPoolName());
return new HikariDataSource(config);
}
}
這裡就不多說了, 就是建立hikari型別的資料來源。
5.DataSourceCreator資料來源建立器
@Slf4j
@Setter
public class DataSourceCreator {
/**
* 是否存在druid
*/
private static Boolean druidExists = false;
/**
* 是否存在hikari
*/
private static Boolean hikariExists = false;
static {
try {
Class.forName(DRUID_DATASOURCE);
druidExists = true;
log.debug("dynamic-datasource detect druid,Please Notice \n " +
"https://github.com/baomidou/dynamic-datasource-spring-boot-starter/wiki/Integration-With-Druid");
} catch (ClassNotFoundException ignored) {
}
try {
Class.forName(HIKARI_DATASOURCE);
hikariExists = true;
} catch (ClassNotFoundException ignored) {
}
}
private BasicDataSourceCreator basicDataSourceCreator;
private JndiDataSourceCreator jndiDataSourceCreator;
private HikariDataSourceCreator hikariDataSourceCreator;
private DruidDataSourceCreator druidDataSourceCreator;
private String globalPublicKey;
/**
* 建立資料來源
*
* @param dataSourceProperty 資料來源資訊
* @return 資料來源
*/
public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
DataSource dataSource;
//如果是jndi資料來源
String jndiName = dataSourceProperty.getJndiName();
if (jndiName != null && !jndiName.isEmpty()) {
dataSource = createJNDIDataSource(jndiName);
} else {
Class<? extends DataSource> type = dataSourceProperty.getType();
if (type == null) {
if (druidExists) {
dataSource = createDruidDataSource(dataSourceProperty);
} else if (hikariExists) {
dataSource = createHikariDataSource(dataSourceProperty);
} else {
dataSource = createBasicDataSource(dataSourceProperty);
}
} else if (DRUID_DATASOURCE.equals(type.getName())) {
dataSource = createDruidDataSource(dataSourceProperty);
} else if (HIKARI_DATASOURCE.equals(type.getName())) {
dataSource = createHikariDataSource(dataSourceProperty);
} else {
dataSource = createBasicDataSource(dataSourceProperty);
}
}
this.runScrip(dataSourceProperty, dataSource);
return dataSource;
}
private void runScrip(DataSourceProperty dataSourceProperty, DataSource dataSource) {
String schema = dataSourceProperty.getSchema();
String data = dataSourceProperty.getData();
if (StringUtils.hasText(schema) || StringUtils.hasText(data)) {
ScriptRunner scriptRunner = new ScriptRunner(dataSourceProperty.isContinueOnError(), dataSourceProperty.getSeparator());
if (StringUtils.hasText(schema)) {
scriptRunner.runScript(dataSource, schema);
}
if (StringUtils.hasText(data)) {
scriptRunner.runScript(dataSource, data);
}
}
}
/**
* 建立基礎資料來源
*
* @param dataSourceProperty 資料來源引數
* @return 資料來源
*/
public DataSource createBasicDataSource(DataSourceProperty dataSourceProperty) {
if (StringUtils.isEmpty(dataSourceProperty.getPublicKey())) {
dataSourceProperty.setPublicKey(globalPublicKey);
}
return basicDataSourceCreator.createDataSource(dataSourceProperty);
}
/**
* 建立JNDI資料來源
*
* @param jndiName jndi資料來源名稱
* @return 資料來源
*/
public DataSource createJNDIDataSource(String jndiName) {
return jndiDataSourceCreator.createDataSource(jndiName);
}
/**
* 建立Druid資料來源
*
* @param dataSourceProperty 資料來源引數
* @return 資料來源
*/
public DataSource createDruidDataSource(DataSourceProperty dataSourceProperty) {
if (StringUtils.isEmpty(dataSourceProperty.getPublicKey())) {
dataSourceProperty.setPublicKey(globalPublicKey);
}
return druidDataSourceCreator.createDataSource(dataSourceProperty);
}
/**
* 建立Hikari資料來源
*
* @param dataSourceProperty 資料來源引數
* @return 資料來源
* @author 離世庭院 小鍋蓋
*/
public DataSource createHikariDataSource(DataSourceProperty dataSourceProperty) {
if (StringUtils.isEmpty(dataSourceProperty.getPublicKey())) {
dataSourceProperty.setPublicKey(globalPublicKey);
}
return hikariDataSourceCreator.createDataSource(dataSourceProperty);
}
}
其實仔細看,就是整合了前面四種類型的資料來源,通過簡單工廠模式建立實體類。這裡是真正的去呼叫資料來源,開始建立的地方。
通過拆解來看,發現,也並不太難。繼續來看下一個模組。
七、資料來源提供者
資料來源提供者是連線配置檔案和資料來源建立器的橋樑。資料來源提供者先去讀取配置檔案, 將所有的資料來源讀取到DynamicDataSourceProperties物件的datasource屬性中,datasource是一個Map集合,可以用來儲存多種型別的資料來源。
下面先來看一下資料來源提供者的原始碼結構:
裡面一共有四個檔案,AbstractDataSourceProvider是父類,其他類繼承自這個類,下面來看一下他們的結構
1.AbstractDataSourceProvider是整個動態資料來源提供者的的抽象類
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {
@Autowired
private DataSourceCreator dataSourceCreator;
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 pollName = dataSourceProperty.getPoolName();
if (pollName == null || "".equals(pollName)) {
pollName = item.getKey();
}
dataSourceProperty.setPoolName(pollName);
dataSourceMap.put(pollName, dataSourceCreator.createDataSource(dataSourceProperty));
}
return dataSourceMap;
}
}
這裡的成員變數是資料來源資料來源建立者dataSourceCreator. 提供了一個建立資料來源的方法:createDataSourceMap(...), 這個方法的入參是屬性配置檔案datasources, 返回值是建立的資料來源物件結合.
這裡的主要邏輯思想是: 迴圈遍歷從配置檔案讀取的多個數據源, 然後根據資料來源的型別, 呼叫DataSourceCreator資料來源建立器去建立(初始化)資料來源, 然後返回已經初始化好的資料來源,將其儲存到map集合中.
2.DynamicDataSourceProvider動態資料來源提供者
/**
* 多資料來源載入介面,預設的實現為從yml資訊中載入所有資料來源 你可以自己實現從其他地方載入所有資料來源
*
*/
public interface DynamicDataSourceProvider {
/**
* 載入所有資料來源
*
* @return 所有資料來源,key為資料來源名稱
*/
Map<String, DataSource> loadDataSources();
}
這是一個抽象類, 裡面就提供了一個抽象方法, 載入資料來源.
3.YmlDynamicDataSourceProvider使用yml配置檔案讀取的方式的動態資料來源提供者
@Slf4j
@AllArgsConstructor
public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider implements DynamicDataSourceProvider {
/**
* 所有資料來源
*/
private Map<String, DataSourceProperty> dataSourcePropertiesMap;
@Override
public Map<String, DataSource> loadDataSources() {
return createDataSourceMap(dataSourcePropertiesMap);
}
}
這個原始碼也是非常簡單, 繼承了AbstractDataSourceProvider抽象類, 實現了DynamicDataSourceProvider介面. 在loadDataSources()方法中, 建立了多資料來源, 並返回多資料來源的map集合.
這裡指的一提的是他的成員變數dataSourcePropertiesMap. 這個變數是什麼時候被賦值的呢? 是在專案啟動, 掃描配置檔案DynamicDataSourceAutoConfiguration的時候被初始化的.
4.DynamicDataSourceAutoConfiguration
/**
* 動態資料來源核心自動配置類
*
* @author TaoYu Kanyuxia
* @see DynamicDataSourceProvider
* @see DynamicDataSourceStrategy
* @see DynamicRoutingDataSource
* @since 1.0.0
*/
@Slf4j
@Configuration
@AllArgsConstructor
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class})
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceAutoConfiguration {
private final DynamicDataSourceProperties properties;
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceProvider dynamicDataSourceProvider() {
Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
return new YmlDynamicDataSourceProvider(datasourceMap);
}
}
在DynamicDataSourceAutoConfiguration的腦袋上, 有一個註解@EnableConfigurationProperties(DynamicDataSourceProperties.class), 這個註解的作用是自動掃描配置檔案,並自動匹配屬性值.
然後,將例項化後的屬性物件賦值給properties成員變數. 下面來看看DynamicDataSourceProperties.java屬性配置檔案.
@Slf4j
@Getter
@Setter
@ConfigurationProperties(prefix = DynamicDataSourceProperties.PREFIX)
public class DynamicDataSourceProperties {
public static final String PREFIX = "spring.datasource.dynamic";
public static final String HEALTH = PREFIX + ".health";
/**
* 必須設定預設的庫,預設master
*/
private String primary = "master";
/**
* 是否啟用嚴格模式,預設不啟動. 嚴格模式下未匹配到資料來源直接報錯, 非嚴格模式下則使用預設資料來源primary所設定的資料來源
*/
private Boolean strict = false;
/**
* 是否使用p6spy輸出,預設不輸出
*/
private Boolean p6spy = false;
/**
* 是否使用seata,預設不使用
*/
private Boolean seata = false;
/**
* 是否使用 spring actuator 監控檢查,預設不檢查
*/
private boolean health = false;
/**
* 每一個數據源
*/
private Map<String, DataSourceProperty> datasource = new LinkedHashMap<>();
/**
* 多資料來源選擇演算法clazz,預設負載均衡演算法
*/
private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
/**
* aop切面順序,預設優先順序最高
*/
private Integer order = Ordered.HIGHEST_PRECEDENCE;
/**
* Druid全域性引數配置
*/
@NestedConfigurationProperty
private DruidConfig druid = new DruidConfig();
/**
* HikariCp全域性引數配置
*/
@NestedConfigurationProperty
private HikariCpConfig hikari = new HikariCpConfig();
/**
* 全域性預設publicKey
*/
private String publicKey = CryptoUtils.DEFAULT_PUBLIC_KEY_STRING;
}
這個檔案的功能:
- 這個檔案定義了掃描yml配置檔案的屬性字首:spring.datasource.dynamic,
- 設定了預設的資料庫是master主庫, strict表示是否嚴格模式: 如果是嚴格模式,那麼沒有配置資料庫,卻呼叫了會拋異常, 如果非嚴格模式, 沒有配資料庫, 會採用預設的主資料庫.
- datasource: 用來儲存讀取到的資料來源, 可能有多個數據源, 所以是map的格式
- strategy: 這裡定義了負載均衡策略, 採用的是策略設計模式: 可以在配置檔案中定義, 如果有多個數據源匹配,如何選擇. 可選方案: 1. 負載均衡策略, 2. 隨機策略.
其他引數就不多說, 比較簡單, 見名思意. 以上就是資料來源提供者的主要內容了.
八、動態路由資料來源
這一塊主要功能是在呼叫的時候, 進行動態選擇資料來源。其原始碼結構如下圖
我們知道動態資料來源可以巢狀,為什麼可以巢狀呢,就是這裡決定的, 這裡一共有四個檔案,
1.AbstractRoutingDataSource: 抽象的路由資料來源, 這個類主要作用是在找到目標資料來源的情況下,連線資料庫.
2.DynamicGroupDataSource:動態分組資料來源, 在一個請求連結下的所有資料來源就是一組. 也就是一個請求過來, 可以巢狀資料來源, 這樣資料來源就有多個, 這多個就是一組.
- DynamicRoutingDataSource: 動態路由資料來源, 第一類AbstractRoutingDataSource用來連線資料來源,那麼到底應該連結哪個資料來源呢?在這個類裡面查詢, 如何找呢, 從DynamicDataSourceContextHolder裡面獲取當前執行緒的資料來源. 然後連結資料庫.
- DynamicDataSourceConfigure: 基於多種策略的自動切換資料來源.
這四個檔案的結構關係如下:
先來看看資料來源連線是如何實現的:
1.AbstractRoutingDataSource: 這是一個抽象類, 裡面主要有兩類方法
一類是具體方法,用來進行資料庫連線
另一類是抽象方法, 給出一個抽象方法, 子類實現決定最終資料來源.
public abstract class AbstractRoutingDataSource extends AbstractDataSource {
/**
* 子類實現決定最終資料來源
*
* @return 資料來源
*/
protected abstract DataSource determineDataSource();
@Override
public Connection getConnection() throws SQLException {
return determineDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineDataSource().getConnection(username, password);
}
}
2.DynamicGroupDataSource: 動態分組資料來源,
這裡定義了分組的概念.
- 每一個組有一個組名
- 組裡面有多個數據源, 用list儲存,指的注意的是, list是一個LinkedList,有順序的, 因為在呼叫資料庫查詢資料的時候, 不能調混了,所以使用順序列表集合.
- 選擇資料來源的策略, 有多個數據源,按照什麼策略選擇呢?由策略型別來決定.
public class DynamicGroupDataSource {
private String groupName;
private DynamicDataSourceStrategy dynamicDataSourceStrategy;
private List<DataSource> dataSources = new LinkedList<>();
public DynamicGroupDataSource(String groupName, DynamicDataSourceStrategy dynamicDataSourceStrategy) {
this.groupName = groupName;
this.dynamicDataSourceStrategy = dynamicDataSourceStrategy;
}
public void addDatasource(DataSource dataSource) {
dataSources.add(dataSource);
}
public void removeDatasource(DataSource dataSource) {
dataSources.remove(dataSource);
}
public DataSource determineDataSource() {
return dynamicDataSourceStrategy.determineDataSource(dataSources);
}
public int size() {
return dataSources.size();
}
}
方法的含義都比較好理解,向這個組裡新增資料來源,刪除資料來源,根據策略尋找目標資料來源等.
3.DynamicRoutingDataSource: 這是外部呼叫的實現類, 這個類繼承自AbstractRoutingDataSource, 所以可以直接呼叫連結資料庫的方法, 並且要重寫獲取目標資料來源的方法. 同時採用組合的方式呼叫了DynamicGroupDataSource動態分組資料來源.
除此之外, 還有一個非常用來的資訊, 那就是這個類實現了InitializingBean介面,這個介面提供了一個afterPropertiesSet()方法, 這個方法在bean被初始化完成之後就會被呼叫. 這裡也是整個專案能夠被載入的重點.
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
private static final String UNDERLINE = "_";
/**
* 所有資料庫
*/
private final Map<String, DataSource> dataSourceMap = new LinkedHashMap<>();
/**
* 分組資料庫
*/
private final Map<String, DynamicGroupDataSource> groupDataSources = new ConcurrentHashMap<>();
@Setter
private DynamicDataSourceProvider provider;
@Setter
private String primary;
@Setter
private boolean strict;
@Setter
private Class<? extends DynamicDataSourceStrategy> strategy;
private boolean p6spy;
private boolean seata;
@Override
public DataSource determineDataSource() {
return getDataSource(DynamicDataSourceContextHolder.peek());
}
private DataSource determinePrimaryDataSource() {
log.debug("dynamic-datasource switch to the primary datasource");
return groupDataSources.containsKey(primary) ? groupDataSources.get(primary).determineDataSource() : dataSourceMap.get(primary);
}
/**
* 獲取當前所有的資料來源
*
* @return 當前所有資料來源
*/
public Map<String, DataSource> getCurrentDataSources() {
return dataSourceMap;
}
/**
* 獲取的當前所有的分組資料來源
*
* @return 當前所有的分組資料來源
*/
public Map<String, DynamicGroupDataSource> getCurrentGroupDataSources() {
return groupDataSources;
}
/**
* 獲取資料來源
*
* @param ds 資料來源名稱
* @return 資料來源
*/
public DataSource getDataSource(String ds) {
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)) {
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
return dataSourceMap.get(ds);
}
if (strict) {
throw new RuntimeException("dynamic-datasource could not find a datasource named" + ds);
}
return determinePrimaryDataSource();
}
/**
* 新增資料來源
*
* @param ds 資料來源名稱
* @param dataSource 資料來源
*/
public synchronized void addDataSource(String ds, DataSource dataSource) {
if (!dataSourceMap.containsKey(ds)) {
dataSource = wrapDataSource(ds, dataSource);
dataSourceMap.put(ds, dataSource);
this.addGroupDataSource(ds, dataSource);
log.info("dynamic-datasource - load a datasource named [{}] success", ds);
} else {
log.warn("dynamic-datasource - load a datasource named [{}] failed, because it already exist", ds);
}
}
private void addGroupDataSource(String ds, DataSource dataSource) {
if (ds.contains(UNDERLINE)) {
String group = ds.split(UNDERLINE)[0];
if (groupDataSources.containsKey(group)) {
groupDataSources.get(group).addDatasource(dataSource);
} else {
try {
DynamicGroupDataSource groupDatasource = new DynamicGroupDataSource(group, strategy.newInstance());
groupDatasource.addDatasource(dataSource);
groupDataSources.put(group, groupDatasource);
} catch (Exception e) {
log.error("dynamic-datasource - add the datasource named [{}] error", ds, e);
dataSourceMap.remove(ds);
}
}
}
}
@Override
public void afterPropertiesSet() throws Exception {
Map<String, DataSource> dataSources = provider.loadDataSources();
// 新增並分組資料來源
for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
addDataSource(dsItem.getKey(), dsItem.getValue());
}
// 檢測預設資料來源設定
if (groupDataSources.containsKey(primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), primary);
} else if (dataSourceMap.containsKey(primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary);
} else {
throw new RuntimeException("dynamic-datasource Please check the setting of primary");
}
}
}
既然afterPropertiesSet()方法這麼重要, 就來看看他主要做了哪些事情吧.
- 通過資料來源提供器獲取所有的資料來源,
- 將上一步獲得的所有的資料來源新增到 dataSourceMap 和 addGroupDataSource 中. 這裡獲取資料來源的操作就完成
- 順著這個思路, 如何新增到 dataSourceMap 和 addGroupDataSource中的呢?
private void addGroupDataSource(String ds, DataSource dataSource) {
if (ds.contains(UNDERLINE)) {
String group = ds.split(UNDERLINE)[0];
if (groupDataSources.containsKey(group)) {
groupDataSources.get(group).addDatasource(dataSource);
} else {
try {
DynamicGroupDataSource groupDatasource = new DynamicGroupDataSource(group, strategy.newInstance());
groupDatasource.addDatasource(dataSource);
groupDataSources.put(group, groupDatasource);
} catch (Exception e) {
log.error("dynamic-datasource - add the datasource named [{}] error", ds, e);
dataSourceMap.remove(ds);
}
}
}
}
注意第一句話, if (ds.contains(UNDERLINE)) 只有ds中有下劃線才會走分組資料來源. 如果沒有下劃線,則就是按照單個數據源來處理的. 向組裡面新增資料來源就不多說了.
除此之外還有一個非常重要的類:DynamicDataSourceContextHolder
public final class DynamicDataSourceContextHolder {
/**
* 為什麼要用連結串列儲存(準確的是棧)
* <pre>
* 為了支援巢狀切換,如ABC三個service都是不同的資料來源
* 其中A的某個業務要調B的方法,B的方法需要呼叫C的方法。一級一級呼叫切換,形成了鏈。
* 傳統的只設置當前執行緒的方式不能滿足此業務需求,必須使用棧,後進先出。
* </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() {
}
/**
* 獲得當前執行緒資料來源
*
* @return 資料來源名稱
*/
public static String peek() {
return LOOKUP_KEY_HOLDER.get().peek();
}
/**
* 設定當前執行緒資料來源
* <p>
* 如非必要不要手動呼叫,呼叫後確保最終清除
* </p>
*
* @param ds 資料來源名稱
*/
public static void push(String ds) {
LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds);
}
/**
* 清空當前執行緒資料來源
* <p>
* 如果當前執行緒是連續切換資料來源 只會移除掉當前執行緒的資料來源名稱
* </p>
*/
public static void poll() {
Deque<String> deque = LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
/**
* 強制清空本地執行緒
* <p>
* 防止記憶體洩漏,如手動呼叫了push可呼叫此方法確保清除
* </p>
*/
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
}
儲存了當前執行緒裡面所有的資料來源. 使用的是ThreadLocal<Deque
九、總結
以上就是整個資料來源原始碼的全部內容, 內容比較多, 部分功能描述不是特別詳細. 如有任何疑問, 可以留言, 一起研究.