动态数据源切换
通过Spring AOP + 注解来替换当前线程ThreadLocal中的值,并且通过重写AbstractRoutingDataSource类重写determineCurrentLookUpKey()方法,实现动态数据源切换,满足功能实现的代码0侵入性,并且高度解耦,实现可拔插功能效果
1、环境搭建
导入依赖
<!--springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.5.6</version>
</dependency>
<!--aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
配置文件
spring:
datasource:
db1:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/db1?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2b8
username: root
password: root
druid:
test-on-borrow: true
test-while-idle: true
db2:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost/db2?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2b8
username: root
password: root
druid:
test-on-borrow: true
test-while-idle: true
2、动态数据源
动态数据源的实现说白了是对AbstractRoutingDataSource类原理解析,通过导入mysql-connector-java依赖后会引入mysql的DataSource,而DataSource是跟线程绑定的,通过继承AbstractRoutingDataSource类重写determineCurrentLookUpKey()方法获取数据源,所以需要通过创建ThreadLoacl来存储数据源,以空间换时间实现线程隔离,然后再determineCurrentLookupKey()方法中获取ThreadLocal保存的数据源,从而实现数据源的动态切换。
1、AbstractRoutingDataSource解析
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
// 自定义数据源时注入
@Nullable
private Map<Object, Object> targetDataSources;
// 自定义数据源时注入
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
// 启动时依据spring 加载bean的生命周期时执行afterPropertiesSet方法,通过遍历targetDataSources注入
@Nullable
private Map<Object, DataSource> resolvedDataSources;
// 启动时依据spring 加载bean的生命周期时执行afterPropertiesSet方法,通过defaultTargetDataSource注入
@Nullable
private DataSource resolvedDefaultDataSource;
public AbstractRoutingDataSource() {
}
// 在配置动态数据源时,我们会将多个类型为DataSource,以name为key,DataSource为source填充进map后赋值给targetDataSources属性
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
// 在配置动态数据源时,会指定默认数据源
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}
// 启动后加载
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
// 遍历我们配置多数据源,填充到resolvedDataSources中
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
// 默认数据源也会进行填充
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
// 真正实现切换数据源的地方
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 前面我们说到,切换数据源时通过重写了determineCurrentLookupKey方法,方法返回值是通过获取保存在ThreadLocal中值,这个值是在afterPropertiesSet()方法中填充resolvedDataSources的map中充当key,以此获取dataSource
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
// 如果没有重写determineCurrentLookupKey则会使用默认数据源
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
// 重写该方法,返回值为resolvedDataSources中的key
@Nullable
protected abstract Object determineCurrentLookupKey();
}
2、创建ThreadLocal
/**
* @DATE: 2023/02/02 11:09
* @Author: 小爽帅到拖网速
*/
public class DataSourceContextHolder {
/**
* 默认数据源
*/
public static final String DEFAULT_DS = "db1";
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
// 设置数据源名
public static void set(String dbType) {
contextHolder.set(dbType);
}
// 获取数据源名
public static String get() {
if(StringUtils.isBlank(contextHolder.get())) {
return DEFAULT_DS;
}
return contextHolder.get();
}
// 清除数据源名
public static void clear() {
contextHolder.remove();
}
}
3、继承AbstractRoutingDataSource类
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
3、创建数据源交由spring管理
自定义两个以上类型为DataSource的Bean,并且通过@ConfigurationProperties绑定yml文件中的配置信息;
通过配置DynamicDataSource动态数据源,设定默认数据源并将创建的多个数据源以数据源名为key,dataSource的bean为value填充进map,并赋值给AbstractRoutingDataSource的targetDataSources字段,注意DataSource在DynamicDataSource中是以k-v的方式保存,所以通过ThreadLocal来保存key,在必要时获取key在重写AbstractRoutingDataSource的determineCurrentLookupKey方法中获取后,在AbstractRoutingDataSource的determineTargetDataSource进行数据源切换
/**
* @DATE: 2023/02/02 11:04
* @Author: 小爽帅到拖网速
*/
@Configuration
public class DataSourceConfig {
/**
* 数据源db1
* @return
*/
@Bean(name = "db1")
@ConfigurationProperties(prefix = "spring.datasource.db1")
public DataSource dataSource1(){
return DataSourceBuilder.create().build();
}
/**
* 数据源 checkentry
* @return
*/
@Bean(name = "db2")
@ConfigurationProperties(prefix = "spring.datasource.db2")
public DataSource dataSource2(){
return DataSourceBuilder.create().build();
}
/**
* 配置动态数据源,并指定默认数据源
* @return
*/
@Primary
@Bean(name = "dynamicDS1")
public DataSource dataSource(){
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(dataSource2());
// 配置多数据源
HashMap<Object, Object> dsMap = new HashMap<>();
dsMap.put("db1",dataSource1());
dsMap.put("db2",dataSource2());
dynamicDataSource.setTargetDataSources(dsMap);
return dynamicDataSource;
}
/**
* 配置@Transactional注解事务
* @return
*/
@Bean
public PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(dataSource());
}
}
4、切换数据源的时机(Aop)
1、自定义注解
/**
* @DATE: 2023/02/02 12:38
* @Author: 小爽帅到拖网速
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DS {
String value() default "db1"; // 默认数据源
}
/**
*
* @DATE: 2022/10/19 17:04
* @Author: 小爽帅到拖网速
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DsMethod {
String value() default "db1";
}
2、定义切面
通过Aop搭配注解结合ThreadLocal实现动态切换数据源,无代码侵入性,高度解耦
/**
* @DATE: 2023/02/02 12:39
* @Author: 小爽帅到拖网速
*/
@Aspect
@Order(1)
@Component
public class DynamicDataSourceAspect {
/**
* 针对类
* 切换数据源
* @param point
*/
@Before("@within(com.xiaoshuang.annoation.DS)")
public void beforeSwitchDS(JoinPoint point) {
Class<?> aClass = point.getTarget().getClass();
// 默认数据源
String defalutDataSource = DataSourceContextHolder.DEFAULT_DS;
// 判断是否有DS注解
if (aClass.isAnnotationPresent(DS.class)){
DS annotation = aClass.getAnnotation(DS.class);
// 获取注解中数据源名
defalutDataSource = annotation.value();
}
// 切换数据源
DataSourceContextHolder.set(defalutDataSource);
}
@After("@within(com.xiaoshuang.annoation.DS)")
public void afterSwitchDS(JoinPoint point){
// 清空数据源
DataSourceContextHolder.clear();
}
/**
* 针对方法
* 切换数据源
* @param point
*/
@Around("@annotation(com.xiaoshuang.annotation.DsMethod)")
public void methodSwitchDS(ProceedingJoinPoint point) {
try {
MethodSignature sign = (MethodSignature) point.getSignature();
Method method = sign.getMethod();
// 获取默认数据源
String dataSource = DataSourceContextHolder.DEFAULT_DS;
if (method.isAnnotationPresent(DsMethod.class)) {
DsMethod annotation = method.getAnnotation(DsMethod.class);
// 取出注解中的数据源名
dataSource = annotation.value();
}
// 切换数据源
DataSourceContextHolder.set(dataSource);
// 执行方法
point.proceed();
// 清空数据源
DataSourceContextHolder.clear();
} catch (Throwable throwable) {
logger.error("[DynamicDataSourceMethodAspect] is error,message:{}", ExceptionUtils.getMessage(throwable));
}
}
}
5、注解失效的情况
如果在使用spring框架时,出现以下情况,会导致aop拦截失效
@Service
public class Test {
public void a(){
b();
}
@DsMethod("b")
public void b(){
}
}
这是因为spring使用的动态代理,通过了解spring的启动流程后可以知道aop是在bean的生命周期注入的代理类,调用a方式是spring创建的代理类,而a方法调用b(),是调用b()的目标类,并不是代理类,所以这里aop会失效,解决方法时在调用b方法时通过获取当前aop上下文代理类并进行强制类型转换,通过获取aop的代理类,才会使aop拦截生效
@Service
public class Test {
public void a(){
((Test)AopContext.currentProxy()).b();
}
@DsMethod("b")
public void b(){
}
}
以上便是动态切换数据源的流程解析,如有解析不当,欢迎在评论区指出,感谢大家阅读与支持!