本文不涉及原理性介绍,主要是从代码角度看功能如何实现,代码基于SpringBoot实现。

一 核心类AbstractRoutingDataSource

首先介绍下核心类AbstractRoutingDataSource,存在于org.springframework.jdbc.datasource.lookup包下,实现了javax.sql.DataSource接口,这个接口是基于一个lookup key来实现选择不同数据源的。这个类的官方解释如下:

/**
* Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
* calls to one of various target DataSources based on a lookup key. The latter is usually
* (but not necessarily) determined through some thread-bound transaction context.
*
* @author Juergen Hoeller
* @since 2.0.1
* @see #setTargetDataSources
* @see #setDefaultTargetDataSource
* @see #determineCurrentLookupKey()
*/
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

接下来看下我们的实现过程。首先定义一个类继承AbstractRoutingDataSource。

public class DynamicDataSource extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
return DatasourceHolder.getDatasourceKey();
}

}

其中用到的DatasourceHolder是实现“lookup key”的核心,这个类把使用主库还是从库的信息放到了ThreadLocal里,方便当前线程使用。

public class DatasourceHolder {

public static String MASTER = "master";

public static String SLAVE = "slave";

private static final ThreadLocal<String> lookupKey = new ThreadLocal<>();

/**
* 设置Key
*/
static void setLookupKey(String key) {
lookupKey.set(key);
}

/**
* 获取当前线程的Key
*/
public static String getLookupKey() {
return lookupKey.get();
}

/**
* 清空当前线程的Key
*/
static void clearLookupKey() {
lookupKey.remove();
}

}


二 配置及数据源解析创建

有了可以进行多数据源选择的基础,接下来就该看怎么配置及使用多个数据源了,先看下application.yml里关于数据源的配置信息(这里面是自定义的配置结构,省略了连接池的其它配置):

datasource:
types: mysql,postgres
mysql:
master:
driver: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test
username: root
password: 123456
slave:
driver: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test
username: root
password: 123456
postgres:
master:
driver: org.postgresql.Driver
url: jdbc:postgresql://127.0.0.1:5432/test
username: postgres
password: 123456
slave:
driver: org.postgresql.Driver
url: jdbc:postgresql://127.0.0.1:5432/test
username: postgres
password: 123456

核心内容来了,接下来是如何解析这个配置并使其生效,代码如下:

@Component
@ConditionalOnProperty(name = "datasource.types")
public class DatasourceConfig implements ApplicationContextAware {

private Logger logger = LoggerFactory.getLogger(DatasourceConfig.class);

@Resource
private Environment environment;

private ApplicationContext applicationContext;

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

@PostConstruct
public void init() {
String types = environment.getConfigValue("datasource.types");
if (!StringUtils.hasText(types)) {
return;
}

ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();

String[] array = types.split(",");
for (String type : array) {
//初始化
Map<Object, Object> datasourceMap = new HashMap<>();
datasourceMap.put(DatasourceKeyHolder.MASTER, buildDruidDataSource("datasource." + type + ".master", type + "MasterDatasource"));
datasourceMap.put(DatasourceKeyHolder.SLAVE, buildDruidDataSource("datasource." + type + ".slave", type + "SlaveDatasource"));
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(datasourceMap);
dynamicDataSource.afterPropertiesSet();
//Jdbc模板
BeanDefinitionBuilder mysqlBeanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(NamedParameterJdbcTemplate.class);
mysqlBeanDefinitionBuilder.addConstructorArgValue(dynamicDataSource);
defaultListableBeanFactory.registerBeanDefinition(type + "NamedParameterJdbcTemplate", mysqlBeanDefinitionBuilder.getRawBeanDefinition());
}
}

private DruidDataSource buildDruidDataSource(String prefix, String name) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(environment.getConfigValue(prefix + ".url"));
dataSource.setUsername(environment.getConfigValue(prefix + ".username"));
dataSource.setPassword(environment.getConfigValue(prefix + ".password"));
dataSource.setMaxWait(100);

Properties masterProperties = new Properties();
masterProperties.put("socketTimeout", "500");
masterProperties.put("connectTimeout", "500");
dataSource.setConnectProperties(masterProperties);

dataSource.setName(name);
return dataSource;
}
}

解释一下上面的代码,首先根据datasource.types找到有哪些数据库,例子里有mysql和pg,对应的配置值是:mysql,postgres。然后分别找到每个数据库的master和slave配置信息,即:

datasource.mysql.master
datasource.mysql.slave

datasource.postgres.master
datasource.postgres.slave

接下来通过方法buildDruidDataSource构建Druid连接池,最后手动创建Spring的bean并注册bean,代码片段如下(具体的原理请查阅spring相关内容):

ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();
。。。
BeanDefinitionBuilder mysqlBeanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(NamedParameterJdbcTemplate.class);
mysqlBeanDefinitionBuilder.addConstructorArgValue(dynamicDataSource);
defaultListableBeanFactory.registerBeanDefinition(type + "NamedParameterJdbcTemplate", mysqlBeanDefinitionBuilder.getRawBeanDefinition());


三 应用

我们创建一个类抽象类BaseDAO,提供了一个抽象方法datasourceType(),所有的子类需要实现这个方法,同时,返回值为datasource.types配置项的值(按前面的application.yml配置信息,返回值为mysql或postgres),代码如下(只写了两个方法示意,更多的根据实际需要增加):

public abstract class BaseDAO implements ApplicationContextAware {

private ApplicationContext applicationContext;

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

protected abstract String datasourceType();

protected <T> List<T> query(String sql, Map<String, ?> paramMap, RowMapper<T> rowMapper) {
try {
return getJdbcTemplate(false).query(sql, new MapSqlParameterSource(paramMap), rowMapper);
} catch (Exception e) {
//TODO 需要处理异常
} finally {
clear();
}
}

protected int update(String sql, Map<String, ?> paramMap) throws DataAccessException {
try {
return getJdbcTemplate(true).update(sql, new MapSqlParameterSource(paramMap));
} catch (Exception e) {
//TODO 需要处理异常
} finally {
clear();
}
}

private NamedParameterJdbcTemplate getJdbcTemplate(boolean master) {
if (master) {
DatasourceKeyHolder.setDatasourceKey(DatasourceKeyHolder.MASTER);
} else {
DatasourceKeyHolder.setDatasourceKey(DatasourceKeyHolder.MASTER);
}
return applicationContext.getBean(datasourceType() + "NamedParameterJdbcTemplate", NamedParameterJdbcTemplate.class);
}

private void clear() {
DatasourceKeyHolder.clearDatasourceKey();
}

}

解释一下getJdbcTemplate这个方法。首先根据布尔变量master的值判断是否使用主库,来设置lookup key。最终是通过类DynamicDataSource的determineCurrentLookupKey方法生效。在方法的最后一行,返回了当前数据库(即datasourceType()方法返回值对应的数据库类型)对应的NamedParameterJdbcTemplate实例。

一个BaseDAO子类例子如下:

@Repository
public class DummyMySQLDAO extends BaseDAO {

@Override
protected String datasourceType() {
return "mysql";
}

@Override
public <T> List<T> query(String sql, Map<String, ?> paramMap, RowMapper<T> rowMapper) {
return super.query(sql, paramMap, rowMapper);
}

}

结束

如有问题和遗漏,欢迎指正错误和讨论。


     原文在我的公众号“平凡的程序员”。

使用AbstractRoutingDataSource同时支持多种数据库及主从_多数据源