Spring:AbstractRoutingDataSource实现动态数据源

项目地址:https://gitee.com/hong_007/dynamic-datasource
一.由最核心的类说起
在实现动态数据源的过程中,最核心的一个类在我的代码中如下:

package com.example.common;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 取得当前使用哪个数据源
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder.getDbType();
    }
}

可以看到,这里是直接继承了AbstractRoutingDataSource,然后重写了他的抽象方法determineCurrentLookupKey(),剩下的就是交由Spring容器根据AOP或者注解在程序运行时动态切换数据源了.

二.org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
看下com.example.config.MybatisPlusConfig.java类中的代码:

package com.example.config;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.MybatisConfiguration;
import com.baomidou.mybatisplus.entity.GlobalConfiguration;
import com.baomidou.mybatisplus.mapper.LogicSqlInjector;
import com.baomidou.mybatisplus.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.plugins.PerformanceInterceptor;
import com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean;
import com.example.common.DBTypeEnum;
import com.example.common.DynamicDataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
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;

@Configuration
@MapperScan({"com.example.mapper*"})
public class MybatisPlusConfig {

    /**
     * mybatis-plus分页插件<br>
     * 文档:http://mp.baomidou.com<br>
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        //paginationInterceptor.setLocalPage(true);// 开启 PageHelper 的支持
        return paginationInterceptor;
    }

    /**
     * mybatis-plus SQL执行效率插件【生产环境可以关闭】
     */
    @Bean
    public PerformanceInterceptor performanceInterceptor() {
        return new PerformanceInterceptor();
    }

    @Bean(name = "db1")
    @ConfigurationProperties(prefix = "spring.datasource.druid.db1" )
    public DataSource db1 () {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "db2")
    @ConfigurationProperties(prefix = "spring.datasource.druid.db2" )
    public DataSource db2 () {
        return DruidDataSourceBuilder.create().build();
    }
    /**
     * 动态数据源配置
     * @return
     */
    @Bean
    @Primary
    public DataSource multipleDataSource (@Qualifier("db1") DataSource db1,
                                          @Qualifier("db2") DataSource db2 ) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map< Object, Object > targetDataSources = new HashMap<>();
        targetDataSources.put(DBTypeEnum.db1.getValue(), db1 );
        targetDataSources.put(DBTypeEnum.db2.getValue(), db2);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        //设置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(db1);
        return dynamicDataSource;
    }

    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(multipleDataSource(db1(),db2()));
        //sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*/*Mapper.xml"));

        MybatisConfiguration configuration = new MybatisConfiguration();
        //configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        sqlSessionFactory.setConfiguration(configuration);
        sqlSessionFactory.setPlugins(new Interceptor[]{ //PerformanceInterceptor(),OptimisticLockerInterceptor()
                paginationInterceptor()
        });
        sqlSessionFactory.setGlobalConfig(globalConfiguration());
        return sqlSessionFactory.getObject();
    }

    @Bean
    public GlobalConfiguration globalConfiguration() {
        GlobalConfiguration conf = new GlobalConfiguration(new LogicSqlInjector());
        conf.setLogicDeleteValue("-1");
        conf.setLogicNotDeleteValue("1");
        conf.setIdType(0);
        conf.setMetaObjectHandler(new MyMetaObjectHandler());
        conf.setDbColumnUnderline(true);
        conf.setRefresh(true);
        return conf;
    }
}

在 multipleDataSource (@Qualifier(“db1”) DataSource db1,@Qualifier(“db2”) DataSource db2 ),既然是多数据源,那么对于javax.sql.DataSource就会有多个实现,可以看到方法参数中用@Qualifier标明了不同实现在容器中的唯一beanName.
@Primary和@Qualifier的不同一句话总结:

  • @Primary: 意思是在众多相同的bean中,优先使用用@Primary注解的bean
  • @Qualifier : 这个注解则指定某个bean有没有资格进行注入

我们看下这两句代码:

  dynamicDataSource.setTargetDataSources(targetDataSources);
  //设置默认数据源
  dynamicDataSource.setDefaultTargetDataSource(db1);

然后看下对应的AbstractRoutingDataSource.java对应的field:
这里写图片描述

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();

    @Nullable
    private Map<Object, DataSource> resolvedDataSources;

    @Nullable
    private DataSource resolvedDefaultDataSource;
    ....
    }

AbstractRoutingDataSource位于org.springframework.jdbc.datasource.lookup包下,这里又可以牵扯出Spring中lookup-method方法注入,暂且不讨论.
代码中将多数据源存储到了Map

    @Bean(name = "db1")
    @ConfigurationProperties(prefix = "spring.datasource.druid.db1" )
    public DataSource db1 () {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "db2")
    @ConfigurationProperties(prefix = "spring.datasource.druid.db2" )
    public DataSource db2 () {
        return DruidDataSourceBuilder.create().build();
    }

同时设置了默认数据源defaultTargetDataSource.
接下来是核心方法determineCurrentLookupKey(),在实现中由DbContextHolder.getDbType()返回当前线程所持有的DataSource对应的key,DbContextHolder是一个由ThreadLocal实现的线程安全的数据源容器.debug进入AbstractRoutingDataSource类中,

@Nullable
    protected abstract Object determineCurrentLookupKey();

这里是用到了模板设计模式,Spring中大量用到了这种模式.既然是个模板方法,那么在这个抽象类构成的整体算法骨架下肯定是用到了这个方法,会找到如下的方法:

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;
    }

这里才是真正动态切换数据源的地方.首先获取到当前线程持有的数据源beanName,由beanName找到容器中的DataSource,接着会在如下代码中就能拿到数据库连接了.

@Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

下面补充看下如何拿到DataSource?
关键一句:DataSource dataSource = this.resolvedDataSources.get(lookupKey);
首先,他的声明如下:

@Nullable
    private Map<Object, DataSource> resolvedDataSources;

也是一个Map,那就好办,看看是哪里给他塞了entry进去,找到了如下方法:

@Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = resolveSpecifiedLookupKey(key);
            DataSource dataSource = resolveSpecifiedDataSource(value);
            this.resolvedDataSources.put(lookupKey, dataSource);
        });
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }

实现了org.springframework.beans.factory.InitializingBean中的afterPropertiesSet()方法,这个方法有什么作用呢?这里涉及到Spring中bean的初始化方式.spring为bean提供了两种初始化bean的方式,实现InitializingBean接口,实现afterPropertiesSet方法,或者在配置文件中同过init-method指定,两种方式可以同时使用.实现InitializingBean接口是直接调用afterPropertiesSet方法,比通过反射调用init-method指定的方法效率相对来说要高点。但是init-method方式消除了对spring的依赖,如果调用afterPropertiesSet方法时出错,则不调用init-method指定的方法.
有个疑问?既然在targetDataSources中已经存好了数据源,为什么还要再弄个resolvedDataSources呢?看下两个Map的参数泛型,一个是Map

protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
        if (dataSource instanceof DataSource) {
            return (DataSource) dataSource;
        }
        else if (dataSource instanceof String) {
            return this.dataSourceLookup.getDataSource((String) dataSource);
        }
        else {
            throw new IllegalArgumentException(
                    "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
        }
    }

这是一个protected方法,所以他是可以被子类重写的.在这个方法中,首先判断targetDataSources中的value是不是DataSource实例,是的话就直接返回了.接着看下面的判断,如果targetDataSources中的value是String类型, 则从dataSourceLookup中返回DataSource.否则就抛出异常.这里也可以看出,默认存储的value要么是DataSource实例,要么是String类型.那dataSourceLookup又是什么呢?

private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

JNDI:参考:https://www.cnblogs.com/xdp-gacl/p/3951952.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值