technology-integration全面解析

technology-integration(五)---Spri

2018-08-23  本文已影响1308人  海苔胖胖

零入侵构造多数据源

  中大型项目常常会配有多个数据库,而需要使用数据库做curd的我们需要一种能很好的解决多数据库连接的问题。零入侵并不是说不需要写代码,而是在不改动原代码的基础上实现需要的功能。
  本章所讲的多数据源是采用ThreadLocal(线程变量)实现的,在应用需要做增删改查的时候,会先获取当前线程的线程变量,根据获取到的线程变量来选择对应的数据源。具体的执行流程如下:

  1. 为线程A的ThreadLocal赋值
  2. 连接DynamicDataSource数据源
  3. DynamicDataSource数据源根据ThreadLocal所持有的值去选择通过DataSource1还是DataSource2执行该SQL语句

开始构造多数据源

由于多数据源需要配合ThreadLocal来实现,所以数据源的配置以java config的方式配置比较直观,如果之前是按照SpringBoot配置方式配置的可以更换成java config方式再来学习本章。

1.多数据源枚举类

由于demo比较简单,所以直接使用1、2的方式命名,需要多少个数据源就添加多少个

public enum  DatabaseType {
    DATASOURCE1,DATASOURCE2
}
2. ThreadLocal

新建DatabaseContextHolder 类,该类持有ThreadLocal对象

public class DatabaseContextHolder {
    private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>();
    public static void setDatabaseType(DatabaseType type) {
        contextHolder.set(type);
    }
    public static DatabaseType getDatabaseType() {
        return contextHolder.get();
    }
}

3、Spring动态数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DatabaseContextHolder.getDatabaseType();
    }
}
4.application.yml添加多数据源配置
datasource1.jdbc:
  driverClassName: com.mysql.jdbc.Driver
  jdbcUrl: jdbc:mysql://localhost:3306/technology-integration
  username: root
  password: 123456
  min-idle: 10

datasource2.jdbc:
  driverClassName: com.mysql.jdbc.Driver
  jdbcUrl: jdbc:mysql://localhost:3306/technology-integration2
  username: root
  password: 123456
  min-idle: 10
5.MybatisConfig配置

修改之前创建的MyBatisConfig类


@Configuration
@MapperScan("com.viu.technology.mapper")
public class MybatisConfig extends HikariConfig{
    @Autowired
    private Environment env;

    //使用Hikaricp数据源
    @Bean("datasource1")
    @ConfigurationProperties(prefix = "datasource1.jdbc")
    public DataSource dataSource1() {
        log.info("开始配置DataSource数据源1");
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName(env.getProperty("datasource1.jdbc.driverClassName"));
        dataSource.setJdbcUrl(env.getProperty("datasource1.jdbc.url"));
        dataSource.setUsername(env.getProperty("datasource1.jdbc.username"));
        dataSource.setPassword(env.getProperty("datasource1.jdbc.password"));
        dataSource.setMaximumPoolSize(100);
        dataSource.setPoolName("hikari-");
        return dataSource;
    }

    //配置第二个数据源
    @Bean("datasource2")
    @ConfigurationProperties(prefix = "datasource2.jdbc")
    public DataSource dataSource2() {
        log.info("开始配置DataSource数据源2");
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName(env.getProperty("datasource2.jdbc.driverClassName"));
        dataSource.setJdbcUrl(env.getProperty("datasource2.jdbc.url"));
        dataSource.setUsername(env.getProperty("datasource2.jdbc.username"));
        dataSource.setPassword(env.getProperty("datasource2.jdbc.password"));
        return dataSource;
    }


    //配置多数据源,使用ThreadLocal对对应的线程保存不用的类型,根据类型获取不同的数据源
    @Bean("datasource")
    @Primary
    public DynamicDataSource dataSource(@Qualifier("datasource1") DataSource dataSource1, 
@Qualifier("datasource2") DataSource dataSource2) {
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DatabaseType.DATASOURCE1, dataSource1);
        targetDataSource.put(DatabaseType.DATASOURCE2, dataSource2);

        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSource);
        dataSource.setDefaultTargetDataSource(dataSource1);
        return dataSource;
    }

    @Bean
    @Primary
    public SqlSessionFactory sqlSessionFactory( DataSource dataSource) throws Exception {
        SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
        fb.setDataSource(dataSource);
        fb.setTypeAliasesPackage("com.viu.technology.po");
        fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapping/*.xml"));
        return fb.getObject();
    }
}


接下来主要解析一下AbstractRoutingDataSource 这个类,这个类是Spring框架自带的一个动态切换数据源的类。

首先需要看一下我们刚刚在MybatisConfig中注册的DynamicDataSource这个Bean

温馨提示:DynamicDataSource也是DataSource的实现类

  1. 先是把两个数据源添加到targetDataSource这个Map集合中
  2. 接着实例化出DynamicDataSource这个类
  3. 设置dynamicDataSource的目标数据源(也就是我们在MybatisConfig中注册的两个数据源)
  4. 设置dynamicDataSource默认使用的数据源,也就是没有为ThreadLocal赋值的情况下默认使用的的数据源
  5. 返回dynamicDataSource,完成DataSource Bean的注册


    DynamicDataSource

注册多数据源的整合主要就是在dynamicDataSource的实现,接下来我们Ctrl+鼠标左键进入AbstractRoutingDataSource类的源码中


image.png

afterPropertiesSet()是AbstractRoutingDataSource的核心方法,我们则从这个方法开始解析代码
public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        } else {
            this.resolvedDataSources = new HashMap(this.targetDataSources.size());
            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);
            }
        }
    }

可以看到这句代码,如果目标数据源为空则会抛出IllegalArgumentException异常,而我们刚才已经在MybatisConfig类里面的dynamicDataSource()为其注入了两个数据源

this.targetDataSources == null
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(DatabaseType.DATASOURCE1, dataSource1);
targetDataSource.put(DatabaseType.DATASOURCE2, dataSource2);
DynamicDataSource dataSource = new DynamicDataSource();

如果this.targetDataSources不为null,afterPropertiesSet()方法,则会将targetDataSource转换为resolvedDataSources 。主要就是将targetDataSource中所有的value由Object对象转换为DataSource对象,两个map中的key存放的是我们自定义的DatabaseType 枚举类。这里的forEach使用的是lambda表达式,等同于foreach循环

private Map<Object, Object> targetDataSources;
private Map<Object, DataSource> resolvedDataSources;
 this.resolvedDataSources = new HashMap(this.targetDataSources.size());
            this.targetDataSources.forEach((key, value) -> {
                Object lookupKey = this.resolveSpecifiedLookupKey(key);
                DataSource dataSource = this.resolveSpecifiedDataSource(value);
                this.resolvedDataSources.put(lookupKey, dataSource);
            });

下面的代码主要的操作使用resolveSpecifiedDataSource()方法,将Object对象转换为DataSource对象。如果Object对象为DataSource的实例,则直接强转;如果Object对象为String的实例,则根据jndi的名字获取DataSource;否则将抛出异常

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

最后AbstractRoutingDataSource会根据defaultTargetDataSource是否为空为resolvedDefaultDataSource赋值,resolvedDefaultDataSource是转变后的DataSource默认使用的数据源

if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }

多数据源的存储解析到此为止,接下来我们看看AbstractRoutingDataSource是如何切换数据源的。
这个方法则是AbstractRoutingDataSource切换数据源的关键,让我们来一步步解析该方法。

protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource 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 + "]");
        } else {
            return dataSource;
        }
    }

判断resolvedDataSources是否为空,如果为空则抛出IllegalArgumentException异常

 Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");

这个方法为读取resolvedDataSources的key,用于获取对应的DataSource

Object lookupKey = this.determineCurrentLookupKey();

我们可以看到该类下的方法为抽象方法,而我们继承AbstractRoutingDataSource的DynamicDataSource类则实现了这个抽象方法,方法返回了当前线程所持有的线程变量,也就是DatabaseType其中的值。

protected abstract Object determineCurrentLookupKey();
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DatabaseContextHolder.getDatabaseType();
    }
}

determineTargetDataSource()根据实现类重写的determineCurrentLookupKey()方法获取到了当前线程所持有的线程变量,根据获取到的线程变量取出resolvedDataSources存储的DataSource。

DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);

如果从resolvedDataSources获取到的DataSource为空,且当前的线程变量为空、lenientFallback 为true的情况下会使用resolvedDefaultDataSource中的DataSource进行数据操作。lenientFallback 的意思是,当datasource这个变量为空的时候会使用resolvedDefaultDataSource进行数据源操作。

if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
       dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
      throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
 } else {
      return dataSource;
}

getConnection那些方法就不进行解析了,以上所有对源代码的解读均为个人理解,如有错误欢迎指出。

Dao中切换数据源

只需要在执行SQL语句前调用setDatabaseType方法为线程本地变量contextHolder赋值即可

/**
     * 使用数据源2
     * @param id
     * @return
     */
    public User selectUserById(String id) {
        DatabaseContextHolder.setDatabaseType(DatabaseType.DATASOURCE2);
        return userMapper.selectByPrimaryKey(id);
    }

更多文章请关注该 technology-integration全面解析专题

上一篇下一篇

猜你喜欢

热点阅读