日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
手把手教你玩多數(shù)據(jù)源動(dòng)態(tài)切換!

我在 19 年的時(shí)候?qū)戇^幾篇文章教大家配置 JdbcTemplate、MyBatis 以及 JPA 中的多數(shù)據(jù)源(公眾號江南一點(diǎn)雨后臺(tái)回復(fù) 666 有相關(guān)的資料),不過那幾篇文章的整體思路都是弄多個(gè) Dao 層實(shí)例,然后手動(dòng)選擇用哪個(gè)實(shí)例,這樣總感覺不太方便。

MyBatis-Plus 也提供了相應(yīng)的工具,感興趣的小伙伴可以自行嘗試。

今天我想帶領(lǐng)小伙伴們,利用 AOP 的思想,自己來寫一個(gè)簡單的多數(shù)據(jù)源切換工具。

1. 預(yù)備知識

想要自定義動(dòng)態(tài)數(shù)據(jù)源切換,得先了解一個(gè)類 AbstractRoutingDataSource:

AbstractRoutingDataSource 是在 Spring2.0.1 中引入的(注意是 Spring2.0.1 不是 Spring Boot2.0.1,所以這其實(shí)也算是 Spring 一個(gè)非常古老的特性了), 該類充當(dāng)了 DataSource 的路由中介,它能夠在運(yùn)行時(shí), 根據(jù)某種 key 值來動(dòng)態(tài)切換到真正的 DataSource 上。

大致的用法就是你提前準(zhǔn)備好各種數(shù)據(jù)源,存入到一個(gè) Map 中,Map 的 key 就是這個(gè)數(shù)據(jù)源的名字,Map 的 value 就是這個(gè)具體的數(shù)據(jù)源,然后再把這個(gè) Map 配置到 AbstractRoutingDataSource 中,最后,每次執(zhí)行數(shù)據(jù)庫查詢的時(shí)候,拿一個(gè) key 出來,AbstractRoutingDataSource 會(huì)找到具體的數(shù)據(jù)源去執(zhí)行這次數(shù)據(jù)庫操作。

大致思路就是這樣。

接下來我們就來看看怎么玩。

2. 創(chuàng)建項(xiàng)目

首先我們創(chuàng)建一個(gè) Spring Boot 項(xiàng)目,引入 Web、MyBatis 以及 MySQL 依賴,項(xiàng)目創(chuàng)建成功之后,再手動(dòng)加入 Druid 和 AOP 依賴,如下:


org.springframework.boot
spring-boot-starter-aop


com.alibaba
druid-spring-boot-starter
1.2.9

這塊呢其實(shí)沒啥好說的,都是常規(guī)操作。

3. 配置文件

接下來我們創(chuàng)建一個(gè) application-druid.yaml 用來配置我們的數(shù)據(jù)源信息,如下:

# 數(shù)據(jù)源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
ds:
# 主庫數(shù)據(jù)源,默認(rèn) master 不能變
master:
url: jdbc:mysql://127.0.0.1:3306/test08?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123
# 從庫數(shù)據(jù)源
slave:
url: jdbc:mysql://127.0.0.1:3306/test07?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123
# 初始連接數(shù)
initialSize: 5
# 最小連接池?cái)?shù)量
minIdle: 10
# 最大連接池?cái)?shù)量
maxActive: 20
# 配置獲取連接等待超時(shí)的時(shí)間
maxWait: 60000
# 配置間隔多久才進(jìn)行一次檢測,檢測需要關(guān)閉的空閑連接,單位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一個(gè)連接在池中最小生存的時(shí)間,單位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一個(gè)連接在池中最大生存的時(shí)間,單位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置檢測連接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
druid:
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 設(shè)置白名單,不填則允許所有訪問
allow:
url-pattern: /druid/*
# 控制臺(tái)管理用戶名和密碼
login-username: javaboy
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL記錄
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true

都是 Druid 的常規(guī)配置,也沒啥好說的,唯一需要注意的是我們整個(gè)配置文件的格式。ds 里邊配置我們的所有數(shù)據(jù)源,每個(gè)數(shù)據(jù)源都有一個(gè)名字,master 是默認(rèn)數(shù)據(jù)源的名字,不可修改,其他數(shù)據(jù)源都可以自定義名字。最后面我們還配置了 Druid 的監(jiān)控功能,如果小伙伴們還不懂 Druid 的監(jiān)控功能,可以查看Spring Boot 如何監(jiān)控 SQL 運(yùn)行情況?。

不過小伙伴們知道,YAML 配置不像 properties 配置可以通過 @PropertySource 注解加載自定義的配置文件,YAML 配置沒有類似的加載機(jī)制。不過工具是死的人是活的,我們可以利用 Spring Boot 的 profile 機(jī)制來加載這個(gè)自定義的 application-druid.yaml 配置文件,具體做法就是在 application.yaml 中加一行配置,如下:

spring:
profiles:
active: druid

接下來我們還需要提供一個(gè)配置類,將這個(gè)配置文件的內(nèi)容加載到配置類中,如下:

@ConfigurationProperties(prefix = "spring.datasource")
public class DruidProperties {
private int initialSize;

private int minIdle;

private int maxActive;

private int maxWait;

private int timeBetweenEvictionRunsMillis;

private int minEvictableIdleTimeMillis;

private int maxEvictableIdleTimeMillis;

private String validationQuery;

private boolean testWhileIdle;

private boolean testOnBorrow;

private boolean testOnReturn;

private Map> ds;

public DruidDataSource dataSource(DruidDataSource datasource) {
/** 配置初始化大小、最小、最大 */
datasource.setInitialSize(initialSize);
datasource.setMaxActive(maxActive);
datasource.setMinIdle(minIdle);

/** 配置獲取連接等待超時(shí)的時(shí)間 */
datasource.setMaxWait(maxWait);

/** 配置間隔多久才進(jìn)行一次檢測,檢測需要關(guān)閉的空閑連接,單位是毫秒 */
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

/** 配置一個(gè)連接在池中最小、最大生存的時(shí)間,單位是毫秒 */
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);

/**
* 用來檢測連接是否有效的sql,要求是一個(gè)查詢語句,常用select 'x'。如果validationQuery為null,testOnBorrow、testOnReturn、testWhileIdle都不會(huì)起作用。
*/
datasource.setValidationQuery(validationQuery);
/** 建議配置為true,不影響性能,并且保證安全性。申請連接的時(shí)候檢測,如果空閑時(shí)間大于timeBetweenEvictionRunsMillis,執(zhí)行validationQuery檢測連接是否有效。 */
datasource.setTestWhileIdle(testWhileIdle);
/** 申請連接時(shí)執(zhí)行validationQuery檢測連接是否有效,做了這個(gè)配置會(huì)降低性能。 */
datasource.setTestOnBorrow(testOnBorrow);
/** 歸還連接時(shí)執(zhí)行validationQuery檢測連接是否有效,做了這個(gè)配置會(huì)降低性能。 */
datasource.setTestOnReturn(testOnReturn);
return datasource;
}

public int getInitialSize() {
return initialSize;
}

public void setInitialSize(int initialSize) {
this.initialSize = initialSize;
}

public int getMinIdle() {
return minIdle;
}

public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}

public int getMaxActive() {
return maxActive;
}

public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
}

public int getMaxWait() {
return maxWait;
}

public void setMaxWait(int maxWait) {
this.maxWait = maxWait;
}

public int getTimeBetweenEvictionRunsMillis() {
return timeBetweenEvictionRunsMillis;
}

public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) {
this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
}

public int getMinEvictableIdleTimeMillis() {
return minEvictableIdleTimeMillis;
}

public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) {
this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
}

public int getMaxEvictableIdleTimeMillis() {
return maxEvictableIdleTimeMillis;
}

public void setMaxEvictableIdleTimeMillis(int maxEvictableIdleTimeMillis) {
this.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
}

public String getValidationQuery() {
return validationQuery;
}

public void setValidationQuery(String validationQuery) {
this.validationQuery = validationQuery;
}

public boolean isTestWhileIdle() {
return testWhileIdle;
}

public void setTestWhileIdle(boolean testWhileIdle) {
this.testWhileIdle = testWhileIdle;
}

public boolean isTestOnBorrow() {
return testOnBorrow;
}

public void setTestOnBorrow(boolean testOnBorrow) {
this.testOnBorrow = testOnBorrow;
}

public boolean isTestOnReturn() {
return testOnReturn;
}

public void setTestOnReturn(boolean testOnReturn) {
this.testOnReturn = testOnReturn;
}

public Map> getDs() {
return ds;
}

public void setDs(Map> ds) {
this.ds = ds;
}
}

這個(gè)配置類沒啥好說的,我們配置的多個(gè)數(shù)據(jù)源我將之讀取到了一個(gè)名為 ds 的 Map 中,將來就根據(jù)這個(gè) Map 中的數(shù)據(jù)來構(gòu)造數(shù)據(jù)源。

4. 加載數(shù)據(jù)

源接下來我們要根據(jù)配置文件來加載數(shù)據(jù)源。加載方式如下:

public interface DynamicDataSourceProvider {
String DEFAULT_DATASOURCE = "master";
/**
* 加載所有的數(shù)據(jù)源
* @return
*/
Map loadDataSources();
}
@Configuration
@EnableConfigurationProperties(DruidProperties.class)
public class YamlDynamicDataSourceProvider implements DynamicDataSourceProvider {
@Autowired
DruidProperties druidProperties;

@Override
public Map loadDataSources() {
Map ds = new HashMap<>(druidProperties.getDs().size());
try {
Map> map = druidProperties.getDs();
Set keySet = map.keySet();
for (String s : keySet) {
DruidDataSource dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(map.get(s));
ds.put(s, druidProperties.dataSource(dataSource));
}
} catch (Exception e) {
e.printStackTrace();
}
return ds;
}
}

加載的核心工作在 YamlDynamicDataSourceProvider 類中完成的。該類中有一個(gè) loadDataSources 方法表示讀取所有的數(shù)據(jù)源對象。數(shù)據(jù)源的相關(guān)屬性都在 druidProperties 對象中,我們先根據(jù)基本的數(shù)據(jù)庫連接信息創(chuàng)建一個(gè) DataSource 對象,然后再調(diào)用 druidProperties#dataSource 方法為這些數(shù)據(jù)源連接池配置其他的屬性(最大連接數(shù)、最小空閑數(shù)等),最后,以 key-value 的形式將數(shù)據(jù)源存入一個(gè) Map 集合中,每一個(gè)數(shù)據(jù)源的 key 就是你在 YAML 中配置的數(shù)據(jù)源名稱。

5. 數(shù)據(jù)源切換

對于當(dāng)前數(shù)據(jù)庫操作使用哪個(gè)數(shù)據(jù)源?我們有很多種不同的設(shè)置方案,當(dāng)然最為省事的辦法是把當(dāng)前使用的數(shù)據(jù)源信息存入到 ThreadLocal 中,ThreadLocal 的特點(diǎn),簡單說就是在哪個(gè)線程中存入的數(shù)據(jù),在哪個(gè)線程才能取出來,換一個(gè)線程就取不出來了,這樣可以確保多線程環(huán)境下的數(shù)據(jù)安全。

先來一個(gè)簡單的工具類,如下:

public class DynamicDataSourceContextHolder {
public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);

/**
* 使用ThreadLocal維護(hù)變量,ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本,
* 所以每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)影響其它線程所對應(yīng)的副本。
*/
private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>();

/**
* 設(shè)置數(shù)據(jù)源的變量
*/
public static void setDataSourceType(String dsType) {
log.info("切換到{}數(shù)據(jù)源", dsType);
CONTEXT_HOLDER.set(dsType);
}

/**
* 獲得數(shù)據(jù)源的變量
*/
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}

/**
* 清空數(shù)據(jù)源變量
*/
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}

接下來我們自定義一個(gè)注解用來標(biāo)記當(dāng)前的數(shù)據(jù)源,如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DataSource {
String dataSourceName() default DynamicDataSourceProvider.DEFAULT_DATASOURCE;

@AliasFor("dataSourceName")
String value() default DynamicDataSourceProvider.DEFAULT_DATASOURCE;
}

這個(gè)注解將來加在 Service 層的方法上,使用該注解的時(shí)候,需要指定一個(gè)數(shù)據(jù)源名稱,不指定的話,默認(rèn)就使用 master 作為數(shù)據(jù)源。

我們還需要通過 AOP 來解析當(dāng)前的自定義注解,如下:

@Aspect
@Order(1)
@Component
public class DataSourceAspect {
@Pointcut("@annotation(org.javaboy.demo.annotation.DataSource)"
+ "|| @within(org.javaboy.demo.annotation.DataSource)")
public void dsPc() {

}

@Around("dsPc()")
public Object around(ProceedingJoinPoint point) throws Throwable {
DataSource dataSource = getDataSource(point);

if (Objects.nonNull(dataSource)) {
DynamicDataSourceContextHolder.setDataSourceType(dataSource.dataSourceName());
}

try {
return point.proceed();
} finally {
// 銷毀數(shù)據(jù)源 在執(zhí)行方法之后
DynamicDataSourceContextHolder.clearDataSourceType();
}
}

/**
* 獲取需要切換的數(shù)據(jù)源
*/
public DataSource getDataSource(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (Objects.nonNull(dataSource)) {
return dataSource;
}
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
}

首先,我們在 dsPc() 方法上定義了切點(diǎn),我們攔截下所有帶有 @DataSource 注解的方法,同時(shí)由于該注解也可以加在類上,如果該注解加在類上,就表示類中的所有方法都使用該數(shù)據(jù)源。

接下來我們定義了一個(gè)環(huán)繞通知,首先根據(jù)當(dāng)前的切點(diǎn),調(diào)用 getDataSource 方法獲取到 @DataSource 注解,這個(gè)注解可能來自方法上也可能來自類上,方法上的優(yōu)先級高于類上的優(yōu)先級。如果拿到的注解不為空,則我們在 DynamicDataSourceContextHolder 中設(shè)置當(dāng)前的數(shù)據(jù)源名稱,設(shè)置完成后進(jìn)行方法的調(diào)用;如果拿到的注解為空,那么就直接進(jìn)行方法的調(diào)用,不再設(shè)置數(shù)據(jù)源了(將來會(huì)自動(dòng)使用默認(rèn)的數(shù)據(jù)源)。最后記得方法調(diào)用完成后,從 ThreadLocal 中移除數(shù)據(jù)源。

6. 定義動(dòng)態(tài)數(shù)據(jù)源

接下來我們來自定義一個(gè)動(dòng)態(tài)數(shù)據(jù)源:

public class DynamicDataSource extends AbstractRoutingDataSource {

DynamicDataSourceProvider dynamicDataSourceProvider;

public DynamicDataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
this.dynamicDataSourceProvider = dynamicDataSourceProvider;
Map targetDataSources = new HashMap<>(dynamicDataSourceProvider.loadDataSources());
super.setTargetDataSources(targetDataSources);
super.setDefaultTargetDataSource(dynamicDataSourceProvider.loadDataSources().get(DynamicDataSourceProvider.DEFAULT_DATASOURCE));
super.afterPropertiesSet();
}

@Override
protected Object determineCurrentLookupKey() {
String dataSourceType = DynamicDataSourceContextHolder.getDataSourceType();
return dataSourceType;
}
}

這就是我們文章開頭所說的 AbstractRoutingDataSource 了,該類有一個(gè)方法名為 determineCurrentLookupKey,當(dāng)需要使用數(shù)據(jù)源的時(shí)候,系統(tǒng)會(huì)自動(dòng)調(diào)用該方法,獲取當(dāng)前數(shù)據(jù)源的標(biāo)記,如 master 或者 slave 或者其他,拿到標(biāo)記之后,就可以據(jù)此獲取到一個(gè)數(shù)據(jù)源了。

當(dāng)我們配置 DynamicDataSource 的時(shí)候,需要配置兩個(gè)關(guān)鍵的參數(shù),一個(gè)是 setTargetDataSources,這個(gè)就是當(dāng)前所有的數(shù)據(jù)源,把當(dāng)前所有的數(shù)據(jù)源都告訴給 AbstractRoutingDataSource,這些數(shù)據(jù)源都是 key-value 的形式(將來根據(jù) determineCurrentLookupKey 方法返回的 key 就可以獲取到具體的數(shù)據(jù)源了);另一個(gè)方法是 setDefaultTargetDataSource,這個(gè)就是默認(rèn)的數(shù)據(jù)源,當(dāng)我們執(zhí)行一個(gè)數(shù)據(jù)庫操作的時(shí)候,如果沒有指定數(shù)據(jù)源(例如 Service 層的方法沒有加 @DataSource 注解),那么默認(rèn)就使用這個(gè)數(shù)據(jù)源。

最后,再將這個(gè) bean 注冊到 Spring 容器中,如下:

@Configuration
public class DruidAutoConfiguration {
@Autowired
DynamicDataSourceProvider dynamicDataSourceProvider;

@Bean
DynamicDataSource dynamicDataSource() {
return new DynamicDataSource(dynamicDataSourceProvider);
}

/**
* 去除數(shù)據(jù)源監(jiān)控頁面的廣告
*
* @param properties
* @return
*/
@Bean
@ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) {
// 獲取web監(jiān)控頁面的參數(shù)
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
// 提取common.js的配置路徑
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
// 創(chuàng)建filter進(jìn)行過濾
Filter filter = new Filter() {
@Override
public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String text = Utils.readFromResource("support/http/resources/js/common.js");
text = text.replace("this.buildFooter();", "");
response.getWriter().write(text);
}

@Override
public void destroy() {
}
};
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns(commonJsPattern);
return registrationBean;
}
}

下面,我們還配置了一個(gè)過濾器,這個(gè)過濾器的目的是去除 Druid 監(jiān)控頁面的阿里廣告。

7. 測試

好啦,大功告成,我們再來測試一下,寫一個(gè) UserMapper:

@Mapper
public interface UserMapper {
@Select("select count(*) from user")
Integer count();
}

一個(gè)很簡單的數(shù)據(jù)庫查詢操作。

再來一個(gè) service:

@Service
public class UserService {

@Autowired
UserMapper userMapper;

@DataSource("master")
public Integer master() {
return userMapper.count();
}

@DataSource("slave")
public Integer slave() {
return userMapper.count();
}
}

通過 @DataSource 注解來指定具體操作的數(shù)據(jù)源,如果沒有使用該注解指定,默認(rèn)就使用 master 數(shù)據(jù)源。

最后去單元測試中測一下,如下:

@SpringBootTest
class DynamicDatasourceDemoApplicationTests {

@Autowired
UserService userService;

@Test
void contextLoads() {
System.out.println("userService.master() = " + userService.master());
System.out.println("userService.slave() = " + userService.slave());
}

}

由于我這里 master 和 slave 分別對應(yīng)了不同的庫,這里查詢會(huì)展示出不同的結(jié)果。


分享題目:手把手教你玩多數(shù)據(jù)源動(dòng)態(tài)切換!
當(dāng)前路徑:http://m.5511xx.com/article/cdocdcc.html