当前位置: 首页 > article >正文

Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘(下)

在上一篇博客《Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘》介绍了dynamic-datasource-spring-boot-starter的自动配置类和配置属性类之后,本文继续来剖析多数据源是如何切换的,什么时候切换的。

前文中提到dynamic-datasource-spring-boot-starter的自动配置类DynamicDataSourceAutoConfiguration在初始化的时候,会创建一个Advisor bean。

	@Role(2)
    @Bean
    public Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
        DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(this.properties.isAllowedPublicOnly(), dsProcessor);
        DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
        advisor.setOrder(this.properties.getOrder());
        return advisor;
    }

该bean负责处理带有@DS注解的类或方法。它发挥的作用和通过@Pointcut+@Before或@Around发挥的作用一致。更多细节可见《探索微服务中的权限控制:一次线上问题排查的思考》

public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {

    private final Advice advice;

    private final Pointcut pointcut;

    public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) {
        // 初始化的时候,传入了dynamicDataSourceAnnotationInterceptor
        // 说明拦截器发挥了通知的作用。
        this.advice = dynamicDataSourceAnnotationInterceptor;
        this.pointcut = buildPointcut();
    }

	// 核心方法, 切点获取,类比@Pointcut注解 
    @Override
    public Pointcut getPointcut() {
        return this.pointcut;
    }

    // 核心方法,通知获取, 类比@Before, @After, @Around注解
    @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 = new AnnotationMethodPoint(DS.class);
        return new ComposablePointcut(cpc).union(mpc);
    }
    ……
}

DynamicDataSourceAnnotationAdvisor类在初始化的时候,传入了DynamicDataSourceAnnotationInterceptor类型的拦截器作为advice, 说明该拦截器发挥了通知的作用(类似@Before, @Around)。

public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {

    /**
     * The identification of SPEL.
     */
    private static final String DYNAMIC_PREFIX = "#";

    private final DataSourceClassResolver dataSourceClassResolver;
    private final DsProcessor dsProcessor;

    public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {
        dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly);
        this.dsProcessor = dsProcessor;
    }

	// 核心方法, dsKey就是@DS注解中的值, 如@DS("cusOB"),那么dsKey = "cusOB"
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        String dsKey = determineDatasourceKey(invocation);
        DynamicDataSourceContextHolder.push(dsKey);
        try {
            return invocation.proceed();
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
    private String determineDatasourceKey(MethodInvocation invocation) {
        String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
        return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
    }
}

在invoke方法执行前,会先把该数据源对应的key加入到线程本地变量的双端队列中,如下。

private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
        @Override
        protected Deque<String> initialValue() {
            return new ArrayDeque<>();
        }
    };

在invoke方法执行后,会先把该数据源对应的key从线程本地变量的双端队列中移除。

看到这里,我们知道,dynamic-datasource-spring-boot-starter会通过DynamicDataSourceAnnotationInterceptor 拦截器获取到@DS注解标识的方法或者类的代理。而@DS注解标注的就是数据源的Key。有了该key, Mybatis就可以获取到该key对应的Druid数据库连接池了。具体的策略,dynamic-datasource-spring-boot-starter提供了轮询和随机两种,这里就不再赘述了。

数据源的切换逻辑在DynamicRoutingDataSource类中。它继承了AbstractRoutingDataSource类,该类实现了getConnection()方法。

public abstract class AbstractRoutingDataSource extends AbstractDataSource {

    protected abstract DataSource determineDataSource();

    @Override
    public Connection getConnection() throws SQLException {
        String xid = TransactionContext.getXID();
        if (StringUtils.isEmpty(xid)) {
        	// 核心方法determineDataSource
            return determineDataSource().getConnection();
        } else {
            String ds = DynamicDataSourceContextHolder.peek();
            ConnectionProxy connection = ConnectionFactory.getConnection(ds);
            return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
        }
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        String xid = TransactionContext.getXID();
        if (StringUtils.isEmpty(xid)) {
            return determineDataSource().getConnection(username, password);
        } else {
            String ds = DynamicDataSourceContextHolder.peek();
            ConnectionProxy connection = ConnectionFactory.getConnection(ds);
            return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection(username, password))
                    : connection;
        }
    }
}

DynamicDataSourceAnnotationInterceptor的invoke方法负责放数据源的key,
determineDataSource方法负责从线程本地变量的双端队列中取出数据源的key。

public DataSource determineDataSource() {
        String dsKey = DynamicDataSourceContextHolder.peek();
        return getDataSource(dsKey);
    }

getDataSource方法用来获取具体的数据库连接。

    private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();


	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);
            // map中存了所有的数据库连接信息
            return dataSourceMap.get(ds);
        }
        if (strict) {
            throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
        }
        return determinePrimaryDataSource();
    }

那么问题来了,dataSourceMap的数据库相关信息是什么时候存进去的呢?

大家还记得上一篇博客中,我们提到dynamic-datasource-spring-boot-starter的自动配置类DynamicDataSourceAutoConfiguration在初始化的时候还创建了一个DynamicDataSourceProvider类型的bean嘛?没错,dataSourceMap是它从DynamicDataSourceProperties配置类中获取,然后存储进去的key是数据源的唯一标识,如value是数据库的相关信息,包括url, username, password等

	@Bean
    @ConditionalOnMissingBean 
    // 确保Spring容器中不存在 DynamicDataSourceProvider 类型的 Bean 时才创建该 Bean
    public DynamicDataSourceProvider dynamicDataSourceProvider() {
        Map<String, DataSourceProperty> datasourceMap = this.properties.getDatasource();
        return new YmlDynamicDataSourceProvider(datasourceMap);
    }

	public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider {

    /**
     * 所有数据源
     */
    private final Map<String, DataSourceProperty> dataSourcePropertiesMap;

    @Override
    public Map<String, DataSource> loadDataSources() {
    	// 核心方法
        return createDataSourceMap(dataSourcePropertiesMap);
    }
}

protected Map<String, DataSource> createDataSourceMap(Map<String, DataSourceProperty> dataSourcePropertiesMap) {
		// dataSourcePropertiesMap 就是从DynamicDataSourceProperties配置类中拿到的所有数据库信息
        Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
        for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
            DataSourceProperty dataSourceProperty = item.getValue();
            String poolName = dataSourceProperty.getPoolName();
            if (poolName == null || "".equals(poolName)) {
                poolName = item.getKey();
            }
            dataSourceProperty.setPoolName(poolName);
            dataSourceMap.put(poolName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
        }
        return dataSourceMap;
    }

最后,我们来总结下,dynamic-datasource-spring-boot-starter本质还是通过AOP实现了多数据源的切换,实现思路和编码规范、风格等都值得大家深入研究和学习。先研究透一个,其它的SpringBoot的常用starter就会手到擒来。


http://www.kler.cn/a/458023.html

相关文章:

  • [羊城杯 2024]hiden
  • 从0实现llama3
  • mybatis 和 mybatisPlus 兼容性问题
  • HTML——38.Span标签和字符实体
  • 极品飞车6的游戏手柄设置
  • Lua : Coroutine(协程)
  • 1.flask介绍、入门、基本用法
  • Python-网络爬虫
  • Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin(自测问题解决!)
  • 【每日学点鸿蒙知识】页面反向传值、图片撑满问题、清除Web缓存、Refresh支持swiper、POP颜色没效果
  • 验证ETL程序产生数据的正确性以及确保数据质量的方法
  • 【畅购商城】详情页详情之商品详情
  • Windows下C++使用SQLite
  • 手机联系人 查询 添加操作
  • 【VulnOSv2靶场渗透】
  • Vue.js组件开发-使用Paho MQTT数据传输
  • 德州仪器 cookie _px3 分析
  • BOOST 库在信号处理领域的具体应用及发展前景
  • 基于Springboot的社区老人健康信息管理系统的设计与实现​
  • 如果你的网站是h5网站,如何将h5网站变成小程序-除开完整重做方法如何快速h5转小程序-h5网站转小程序的办法-优雅草央千澈
  • 脱离电路图编程
  • 2413. 最小偶倍数
  • V-Express - 一款针对人像视频生成的开源软件
  • Mac OS
  • 3.基于 Temporal 的 Couchbase 动态 SQL 执行场景
  • 12.25 VScode+jupyter使用