深度理解 Spring 动态数据源切换是如何实现的

更新(不是必读,只为了帮助读者更好的理解执行过程)

  1. 2022-11-16 结合事务 TransactionInterceptor 的执行,剖析数据源是如何切换的
  2. 详细分析为什么,切面要设置 @Order(-9999) 属性

针对点一回答如下

在Spring Boot 项目启动的时候,会去扫描所有配置类,生成一个个的 Bean ,被 @Transaction 标记的方法其对应的类,会被 Aop 增强生成一个增强 Bean,当我们调用增强 Bean 中的方法前,会执行我们的增强逻辑,这些增强器(图一)逻辑就是执行一个个的 Interceptor 等

图一
在这里插入图片描述
在执行 TransactionInterceptor 的时候(涉及到 Spring 事务源码),会为我们开启一个连接(图二)并设置 Commit =false

图二
在这里插入图片描述

其中在获取连接的时候 Spring 为我们提供了一个扩展类(图三),只要我们实现了这个类,对应的 Jdbc 操作就会解析我们自己注入的数据源,完成对数据库的 JDBC 操作,到此切换数据源的完整流程结束

图三
在这里插入图片描述

针对点二回答如下

我们编写的切面执行流程也是,走上图一中的流程,只不过是执行 MethodBeforeAdviceInterceptor 这个增强器(图四),如果不设置 @Order(-9999) 那么 TransactionInterceptor 会先于 MethodBeforeAdviceInterceptor 执行,就会造成一个结果就是,这边都开始开启一个事务,开始获取连接了,由于数据源切换切面此时还未执行,所以此时钩子方法这里不知道该用哪一个数据源去获取连接,就会报错(图五)

图四
在这里插入图片描述
图五
在这里插入图片描述
上述俩个更新点涉及到的源码知识:

aop增强器执行流程,手把手带你debug (文章末尾有干货)

全面解读spring注解事务失效场景,伪代码+图文深度解析spring源码运行过程

前言

小憩是辣么的让人神往,就像备战高考靠窗位置的那个你,肆无忌道的放空自己,望着深蓝色宁静的天空,思考着未来该何去何从,近处一颗高大魁梧的银杏树在炎炎夏日中尽情的摇曳着自己嫩绿的枝丫,迸发出无尽的希望,回想起来一切都是这么的美好。好了今日的杂想到此结束,回归正文,关于动态数据源切换那点事。

引导思考

如果咱们现在生活在互联网刚开始兴起的那个时期,万物堵塞,所有和数据库打交道的操作只能通过 JDBC 来实现,恰巧你是一个技术狂热爱好者,想通过自己的努力封装一套半 ORM 框架,造福千千万程序员,你会如何实现动态数据源切换这个扩展点呢?

  1. 当用户没有动态数据源切换的需求时:框架加载默认数据源给用户使用
  2. 当用户有动态数据源切换的需求时:提供一个官方认证的工具给用户使用,用户只需按照要求将多数据源配置好,系统会加载用户自定义的数据源

对于点一很好办:我们在框架内部默认创建一个名字为 dataSource(Spring Boot 默认数据源:HikariDataSource)、类型为 javax.sql.DataSource 的这么一个 数据源就好了。
对于点二来说:这个工具应该让用户的学习使用成本越低越好。从用户角度上分析,应该没有人希望把多数据源配置配在 Excel、Txt文件中吧,最优解配在 yml 文件中,然后利用 Spring 中的 Environment 类可以很容易加载到多数据源的配置。(题外话:基于 Spring 生态开发,少走弯路 30 年),到底最后是选择使用哪个数据源,设计思路有俩种:

  1. 可以支持加载多个数据源到系统中,写一个抽象类 AbstractDataSourceRoute 里面实现了 DataSource choiceDataSource(String type),将到底是使用哪个数据源的方法交给子类去实现 abstract String type(),这样用户只需实现 AbstractDataSourceRoute 类,指定一下使用哪个数据源,就可以了。伪代码如下
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.sql.DataSource;
import java.util.Map;

public abstract class DataSourceUtil implements DataSource, ApplicationContextAware, DisposableBean {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    DataSource choiceDataSource() {
        //1:通过 applicationContext 获取所有数据源
        Map<String, DataSource> dataSourceMap = applicationContext.getBeansOfType(DataSource.class);
        //2:通过 type 筛选指定要用的数据源
        DataSource dataSource = dataSourceMap.get(type());
        //3:返回用户指定要使用的数据源
        return dataSource;
    }
    abstract String type();
}

2: 限制系统加载一个数据源,扫描到自定义数据源,就不加载默认数据源,反之加载默认数据源

动态数据源切换之应用

先来简单介绍一下啊动态数据源用到的开发场景:

  1. 项目需要与其他系统对接,库是别人,因此需要配置多套数据源
  2. 项目本身根据实际需求设计数据库的时候,用到了多种的数据库,例如 Mysql、Oracle同时使用,当然数据互通是个问题

使用场景介绍完了,接下来简单介绍一下如何使用吧~,就是读取 yml 配置中我们配置的数据源属性,利用@ConfigurationProperties注解完成属性的自动填充,继而注入到 IOC 容器中。这时候系统读取到了多个数据源,但是还不清楚什么时候用哪个数据源呢,因此我们可以编写一个切面,来动态的告知系统该如何选择数据源

@Configuration
public class DataSourceConfig {
    @Bean(name = "master")
    @ConfigurationProperties(prefix = "spring.datasource.druid.master.datasource")
    public DataSource master() {
        return new DruidDataSource();
    }

    @Bean(name = "slave")
    @ConfigurationProperties(prefix = "spring.datasource.druid.salve.datasource")
    public DataSource slave() {
        return new DruidDataSource();
    }

    @Bean
    @Primary
    public DynamicDataSource multipleDataSource(@Qualifier("master") DataSource db1, @Qualifier("slave") DataSource db2) {
        DynamicDataSource multipleDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.getMASTER(), db1);
        targetDataSources.put(DataSourceType.getSALVE(), db2);
        multipleDataSource.setTargetDataSources(targetDataSources);
        multipleDataSource.setDefaultTargetDataSource(db1);
        DynamicDataSourceContextHolder.dataSourceIds.add(DataSourceType.getSALVE());
        DynamicDataSourceContextHolder.dataSourceIds.add(DataSourceType.getMASTER());
        return multipleDataSource;
    }

    @Data
    static class DataSourceType {

        private static String MASTER = "master";
        private static String SALVE = "salve";

        public static String getMASTER() {
            return MASTER;
        }

        public static String getSALVE() {
            return SALVE;
        }
    }
}

Aspect 切面

编写自定义注解 TargetDataSource ,并通过切面拦截它,根据 TargetDataSource 中的属性做判断,选择特定的数据源。里面的逻辑没啥好看的,就是在方法执行之前,获取方法或者类上面 TargetDataSource 中指定的属性,填充到 DynamicDataSourceContextHolder 中,后续我们重写 Spring 为我们提供的数据源选择钩子方法,返回 DynamicDataSourceContextHolder 中的数据就好了。这样一来就实现了,数据源切换的需求了,接下来看下这个钩子方法里面的源码吧~~

@Slf4j
@Aspect
@Order(-10)
@Component
public class AspectWithinAnnotation {

    /**
     * @within:拦截类上的注解
     * @annotation:拦截方法上的注解
     */
    @Before("@within(com.example.oraceldemo.config.aspect.TargetDataSource)||@annotation(com.example.oraceldemo.config.aspect.TargetDataSource)")
    public void changeDataSource(JoinPoint joinPoint) {
        TargetDataSource targetDataSource = getTargetDataSource(joinPoint);
        if (targetDataSource == null || !DynamicDataSourceContextHolder.isContainsDataSource(targetDataSource.name())) {
            log.error("使用默认的数据源 -> " + joinPoint.getSignature());
        } else {
            log.debug("使用数据源:" + targetDataSource.name());
            DynamicDataSourceContextHolder.setDataSourceType(targetDataSource.name());
        }
    }

    @After("@within(com.example.oraceldemo.config.aspect.TargetDataSource)||@annotation(com.example.oraceldemo.config.aspect.TargetDataSource)")
    public void clearDataSource(JoinPoint joinPoint) {
        log.debug("清除数据源 " + getTargetDataSource(joinPoint).name() + " !");
        DynamicDataSourceContextHolder.clearDataSourceType();
    }

    /**
     * 先从方法上获取 TargetDataSource 注解,获取不到从类上面获取
     */
    public TargetDataSource getTargetDataSource(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        TargetDataSource targetDataSource = method.getAnnotation(TargetDataSource.class);
        if (targetDataSource == null) {
            Class<?> declaringClass = method.getDeclaringClass();
            targetDataSource = declaringClass.getAnnotation(TargetDataSource.class);
        }
        return targetDataSource;
    }
}

Spring 扩展点之 AbstractRoutingDataSource

又是这个 Abstract 开头的类~,记住所有以 Abstract 开头的类,他的父类才是真正干活的人,因为详细的逻辑都封装在父类中了,爸爸才是全家的顶梁柱、主心骨啊,所有的风雨、压力都是爸爸来抗,只为了子女脸上洋溢着的灿烂的笑容。

public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 选择数据源钩子方法
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    public DynamicDataSource() {
    }
}

浅浅的 debug 一下源码吧

在钩子方法中打一个断点,看一下它的执行链路,一次往上翻就好了,来到 AbstractRoutingDataSource 中的 determineTargetDataSource 方法,粗略的一看,这代码和本文开头引导思考中的伪代码,不就是一个模子里刻出来的咩~由于代码思路都是一样的,就不过多的做分析了。为了读者方便理解再次贴一遍原话如下~

写一个抽象类 AbstractDataSourceRoute 里面实现了 DataSource choiceDataSource(String type),将到底是使用哪个数据源的方法交给子类去实现 abstract String type(),这样用户只需实现 AbstractDataSourceRoute 类中的钩子方法,指定一下使用哪个数据源,就可以了。

在这里插入图片描述

浅浅的解读一下 resolvedDataSources 吧

上图一从 resolvedDataSources 中根据钩子方法的返回值,获取指定的数据源返回,那么 resolvedDataSources 中的数据源是什么时候注入的呢?答案入下图,利用了 Spring 中的 InitializingBean 接口,在Bean属性填充完毕后,将 targetDataSources 中数据源全部放到 resolvedDataSources 中,这样一来,我们用户只需指定指定钩子方法中的数据源类别,当方法被调用的时候,切面就会截取方法、类上面的自定义注解,填充到 ThreadLocal 中,然后后续的 Mybatis 获取数据源查 DB 的时候,根据钩子方法的返回值,从 resolvedDataSources 中获取指定的数据源然后查 DB 从而实现了,数据源随意切换的效果
在这里插入图片描述

注意点

由于本文中的切面拦截的是自定义注解,且切入点是使用 @within、@annotation来进行修饰的(即只会识别被调用方法对应的类上、或者是被调用方法上是否存在自定义注解),考虑到现在大多数人都在用 MybatisPlus~,正确用法入下图一、二,图三为错误用法,因为,我们调用的 List 方法本质还是 ServiceImpl 类上的,然而ServiceImpl 类上没有有被我们的注解修饰,故此时切面会失效,本文切面解析注解的范围如下图四。当然可能还有人切入点采用 @Before(“execution(* com.example.oraceldemo.service..(…))”) ,也可能出现切面失效、切换数据源失效的情况,大致的分析过程差不多,读者有兴趣可以自行去分析哦~~~
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

@Autowired
private TabStarService tabStarService;

@Test
void xiaomi() {
    List<Goods> list = goodsService.list();
    System.err.println(list);
}

小咸鱼的技术窝

关注不迷路,日后分享更多技术干货,B站、CSDN、微信公众号同名,名称都是(小咸鱼的技术窝)更多详情在主页
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小咸鱼的技术窝

你的鼓励将是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值