在一個項目中使用多個數據源的情況很多,所以動態切換數據源是項目中標配的功能,當然網上有相關的依賴可以使用,比如動態數據源,其依賴爲,
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
今天,不使用現成的API,手動實現一個動態數據源。
一、環境及依賴
在springboot、mybatis-plus的基礎上實現動態數據源切換,
springboot:2.3.3.RELEASE
mybatis-plus-boot-starter:3.5.0
mysql驅動:8.0.32
除了這些依賴外沒有其他的,目標是動態切換數據源。
二、實現思路
先來看下,單數據源的情況。
在使用springboot和mybatis-plus時,我們沒有配置數據源(DataSource),只配置了數據庫相關的信息,便可以連接數據庫進行數據庫的操作,這是爲什麼吶。其實是基於spring-boot的自動配置,也就是autoConfiguration,在自動配置下有DataSourceAutoConfiguration類,該類會生成一個數據源並注入到spring的容器中,這樣就可以使用該數據源提供的連接,訪問數據庫了。
感興趣的小夥伴可以瞭解下這個類的具體實現邏輯。
要實現多數據源,並且可以自動切換。那麼肯定就不能再使用DataSourceAutoConfigurtation了,因爲它只能產生一個數據源,多個數據源要怎麼辦,spring提供了AbstractRoutingDataSource類,該類是一個抽象類,僅有一個抽象方法需要實現
Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context.
Allows for arbitrary keys. The returned key needs to match the stored lookup key type,
as resolved by the resolveSpecifiedLookupKey method.
@Nullable
protected abstract Object determineCurrentLookupKey();
可以根據該類實現一個動態數據源。好了,現在瞭解了實現思路,開始實現一個動態數據源,要做以下的準備工作。
1、配置文件;
2、自定義動態數據源;
2.1、配置文件
由於是多數據源,那麼在配置文件中肯定是多個配置,不能再是一個數據庫的配置了,這裏使用兩個mysql的配置進行演示,
#master 默認數據源
spring:
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
#slave 從數據源
slave:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/test2?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
這裏使用了一個master一個slave兩個數據源配置,其地址是一致的,但數據庫示例不一樣。 有了數據源的信息下一步要實現自己的數據源,
2.2、自定義動態數據源
前邊說,spring提供了AbstractRoutingDataSource類可以實現動態數據源,看下實現。
DynamicDatasource.java
package com.wcj.my.config.dynamic.source;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 動態數據源
* @date 2023/6/8 19:18
*/
public class DynamicDatasource extends AbstractRoutingDataSource {
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDatasourceHolder.getDataSource();
}
}
這裏的determineCurrentLookupKey方法,需要返回一個數據源,也就是說返回一個數據源的映射,這裏返回一個DynamicDatasourceHolder.getDataSource()方法的返回值,DynamicDatasourceHolder是一個保存多個數據源的地方,
DynamicDatasourceHolder.java
package com.wcj.my.config.dynamic.source;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
/**
* @date 2023/6/8 19:42
*/
public class DynamicDatasourceHolder {
//保存數據源的映射
private static Queue<String> queue = new ArrayBlockingQueue<String>(1);
public static String getDataSource() {
return queue.peek();
}
public static void setDataSource(String dataSourceKey) {
queue.add(dataSourceKey);
}
public static void removeDataSource(String dataSourceKey) {
queue.remove(dataSourceKey);
}
}
該類很簡單,使用一個隊列保存數據源的映射,提供獲取/設置數據源的方法。
這裏使用ThreadLocal類更合適,這樣可以實現線程的隔離,一個請求會有一個線程來處理,保證每隔線程使用的數據源是一樣的。
到現在爲止依舊沒有出現如何創建多數據源,下面就來了,不着急。
DynamicDatasourceConfig.java
package com.wcj.my.config.dynamic.source;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @date 2023/6/8 19:51
*/
@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class DynamicDatasourceConfig {
@Bean("master")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDatasource(){
return DataSourceBuilder.create().build();
}
@Bean("slave")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDatasource(){
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dataSource(){
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put("master", masterDatasource());
dataSourceMap.put("slave", slaveDatasource());
DynamicDatasource dynamicDatasource=new DynamicDatasource();
dynamicDatasource.setTargetDataSources(dataSourceMap);
dynamicDatasource.setDefaultTargetDataSource(masterDatasource());
return dynamicDatasource;
}
}
首先,在該類上有個一個@Configuration註解,標明這是一個配置類;
其次,有一個@EnableAutonConfiguration註解,該註解中有個數組類型的exclude屬性,排除不需要自動配置的類,這裏排除的是當然就是DataSourceAutoConfiguration類了;因爲下面會自動生成數據源,不需要自動配置了;
然後,在類中是標有@Bean的方法,這些方法便是生成數據源類,且映射爲”master“、”slave“,可以有多個。使用的是DataSourceBuilder類幫助生成;
最後,生成一個DynamicDatasource,且標有@Primary註解,這裏需要設置”master“、”slave“兩個映射代表的數據源;
這樣便向spring容器中注入了三個數據源,分別是”master“、”slave“代表的數據源,他們是需要實際使用的數據源。還有一個是DynamicDatasource,提供數據源的設置。這三個都是DataSource的子類。
三、使用多數據源
上面已經完成了多數據源的配置,下面看怎麼使用吧,還記得DynamicDatasourceHolder類中有set/get方法嗎,就是使用這個類提供的方法,
UserSerivce.java
package com.wcj.my.service;
import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
import com.wcj.my.dto.UserDto;
import com.wcj.my.entity.User;
import com.wcj.my.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @date 2023/6/8 15:19
*/
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**默認使用master數據源
*/
public boolean saveUser(UserDto userDto) {
User user = new User();
user.setUName(userDto.getName());
user.setUCode(userDto.getCode());
user.setUAge(userDto.getAge());
user.setUAddress(userDto.getAddress());
int num = userMapper.insert(user);
if (num > 0) {
return true;
}
return false;
}
/**
*使用slave數據源
*/
public boolean saveUserSlave(UserDto userDto) {
DynamicDatasourceHolder.setDataSource("slave");
User user = new User();
user.setUName(userDto.getName());
user.setUCode(userDto.getCode());
user.setUAge(userDto.getAge());
user.setUAddress(userDto.getAddress());
int num = userMapper.insert(user);
DynamicDatasourceHolder.removeDataSource("slave");
if (num > 0) {
return true;
}
return false;
}
}
上面的service層方法在調用dao層方法的時候,使用DynamicDatasourceHolder.setDataSource()方法設置了需要使用的數據源, 通過這樣的方式便可以實現動態數據源了。
不知道,小夥伴們有沒有感覺到,這樣每次在調用方法的時候都需要設置數據源是不是很麻煩,有沒有一種更方面的方式,比如說註解。
四、動態數據源註解@DDS
現在來實現一個動態數據源的註解來代替上面的每次都調用DynamicDatasourceHolder.setDataSource()方法來設置數據源。
先看下,@DDS註解的定義
DDS.java
package com.wcj.my.config.dynamic.source.aspect;
import org.springframework.stereotype.Component;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**動態數據源的註解
* 用在類和方法上,方法上的優先級大於類上的
* 默認值是master
* @date 2023/6/9 16:19
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface DDS {
String value() default "master";
}
註解@DDS使用在類和方法上,切方法上的優先級大於類上的。有一個value的屬性,指明使用的數據源,默認是”master“。
實現一個切面,來切@DDS註解
DynamicDatasourceAspect.java
package com.wcj.my.config.dynamic.source.aspect;
import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* 動態數據源切面
* @date 2023/6/9 16:23
*/
@Aspect
@Component
public class DynamicDatasourceAspect {
/**
* 切點,切的是帶有@DDS的註解
*/
@Pointcut("@annotation(com.wcj.my.config.dynamic.source.aspect.DDS)")
public void dynamicDatasourcePointcut(){
}
/**
* 環繞通知
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("dynamicDatasourcePointcut()")
public Object around(ProceedingJoinPoint joinPoint)throws Throwable{
String datasourceKey="master";
//類上的註解
Class<?> targetClass=joinPoint.getTarget().getClass();
DDS annotation=targetClass.getAnnotation(DDS.class);
//方法上的註解
MethodSignature methodSignature=(MethodSignature)joinPoint.getSignature();
DDS annotationMethod=methodSignature.getMethod().getAnnotation(DDS.class);
if(Objects.nonNull(annotationMethod)){
datasourceKey=annotationMethod.value();
}else{
datasourceKey=annotation.value();
}
//設置數據源
DynamicDatasourceHolder.setDataSource(datasourceKey);
try{
return joinPoint.proceed();
}finally {
DynamicDatasourceHolder.removeDataSource(datasourceKey);
}
}
}
這樣一個動態數據源的註解便可以了,看下怎麼使用,
UserServiceByAnnotation.java
package com.wcj.my.service;
import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
import com.wcj.my.config.dynamic.source.aspect.DDS;
import com.wcj.my.dto.UserDto;
import com.wcj.my.entity.User;
import com.wcj.my.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @date 2023/6/8 15:19
*/
@Service
public class UserServiceByAnnotation {
@Autowired
private UserMapper userMapper;
@DDS("master")
public boolean saveUser(UserDto userDto){
User user=new User();
user.setUName(userDto.getName());
user.setUCode(userDto.getCode());
user.setUAge(userDto.getAge());
user.setUAddress(userDto.getAddress());
int num=userMapper.insert(user);
if(num>0){
return true;
}
return false;
}
@DDS("slave")
public boolean saveUserSlave(UserDto userDto){
User user=new User();
user.setUName(userDto.getName());
user.setUCode(userDto.getCode());
user.setUAge(userDto.getAge());
user.setUAddress(userDto.getAddress());
int num=userMapper.insert(user);
if(num>0){
return true;
}
return false;
}
}
使用起來很簡單,在需要切換數據源的方法或類上使用@DDS註解即可,使用value來改變數據源就好了。
五、動態數據源的原理
很多小夥伴可能和我有一樣的疑惑,使用DynamicDatasourceHolder.setDataSource或@DDS就可以設置數據源了,是怎麼實現的,下面分析下,我們指定dao層的Mapper其實是一個代理對象,其會使用mybatis中的sqlSessionTempalte進行數據庫的操作,在sqlSessionTemplate中會使用DefaultSqlSession對象,最終會使用DataSource,而使用了動態數據源的對象中會注入一個DynamicDataSource,在進行數據庫操作時最終會獲得一個數據庫連接,這裏便會使用DynamicDataSource獲得一個連接,由於它繼承了AbstractRoutingDataSource類,看下其getConnection方法,
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
看下determineTargetDataSource()方法,
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");、
//自己實現的,在調用方法時進行了設置,實現動態數據源的目的
Object lookupKey = determineCurrentLookupKey();
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 + "]");
}
return dataSource;
}
看上面的註釋,determineCurrentLookupkey()方法便是在DynamicDatasource類中進行了實現,從而實現了動態設置數據源的目的。
六、總結
本文動手實現了一個動態數據源,並切提供了註解的方式,主要有以下幾點
1、繼承AbstractRoutingDataSource類的determineCurrentLookupkey()方法,動態設置數據源;
2、取消DataSourceAutoConfiguration的自動配置,手動向spring容器中注入多個數據源;
3、基於@DDS註解動態設置數據源;
最後,本文用到的源碼均可通過下方公衆號獲得。