为了帮助理解此原理,在SpringBoot 2.1.17版本上,借助一个具体的功能来展开讨论(这个功能就是通过页面配置数据源基本信息,并进行测试连接)。
前端页面配置数据源名称(唯一标识)、类型(mysql/oracl 等)、ip、端口、数据库名称、用户名、密码。后台根据配置信息生成创建com.alibaba.druid.pool.DruidDataSource 的必要信息。
1.创建 DynamicDataSource 集成AbstractRoutingDataSource
java
/**
* 动态数据源
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 当前已加载的数据源
*/
private Map<Object, Object> currentDataSources = new HashMap<>(8);
private DruidProperties druidProperties;
//=====================================分析1
public DynamicDataSource(DataSource defaultTargetDataSource,
Map<Object, Object> targetDataSources, DruidProperties druidProperties) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
setCurrentDataSources(targetDataSources);
super.afterPropertiesSet();
this.druidProperties = druidProperties;
}
private void setCurrentDataSources(Map<Object, Object> targetDataSources) {
this.currentDataSources = targetDataSources;
}
public Map<Object, Object> getCurrentDataSources() {
return this.currentDataSources;
}
/**
* 重置数据源
*/
private void resetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
public void addDataSource(String username, String password,
String url, String driverClassName, String dsKey) {
if(! currentDataSources.containsKey(dsKey)){//不存在则put
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setUrl(url);
dataSource.setDriverClassName(driverClassName);
dataSource = druidProperties.dataSource(dataSource);
currentDataSources.put(dsKey, dataSource);
resetDataSources(currentDataSources);
}
}
}
2.创建 DynamicDataSourceContextHolder 操作线程ThreadLocal。保存数据源名称(唯一标识)
java
/**
* 数据源切换处理
*
*/
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源的变量
*/
public static void setDataSourceType(String dsType) {
log.info("切换到{}数据源", dsType);
CONTEXT_HOLDER.set(dsType);
}
/**
* 获得数据源的变量
*/
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源变量
*/
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
3.创建DruidConfig 处理初始化时主数据源的加载。
java
/**
* druid 配置多数据源
*/
@Configuration
public class DruidConfig
{
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(DruidProperties druidProperties)
{
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave",
name = "enabled", havingValue = "true")
public DataSource slaveDataSource(DruidProperties druidProperties)
{
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource,
DruidProperties druidProperties)
{
Map<Object, Object> targetDataSources = new HashMap<>(4);
targetDataSources.put("MASTER", masterDataSource);
setDataSource(targetDataSources, "SLAVE", "slaveDataSource");
return new DynamicDataSource(masterDataSource,
targetDataSources, druidProperties);
}
/**
* 设置数据源
*/
public void setDataSource(Map<Object, Object> targetDataSources,
String sourceName, String beanName)
{
try
{
DataSource dataSource = SpringUtils.getBean(beanName);
targetDataSources.put(sourceName, dataSource);
}
catch (Exception e)
{
}
}
}
分析1:
服务启动初始化,加载配置DruidConfig 创建dynamicDataSource ,将application-druid.yml中的主数据源(默认数据)设置到AbstractRoutingDataSource的 默认数据源Object defaultTargetDataSource。
将主数据与和从数据源(如存在)保存到已加载的数据源 Map<Object, Object> targetDataSources中,Map的key为数据源名称(唯一)。执行afterPropertiesSet(),
将默认主数据源赋值resolvedDefaultDataSource,已加载的数据源设置到resolvedDataSources。
因此当服务做查询操作时,调用getConnection()执行determineTargetDataSource()方法,因为DynamicDataSource重写了AbstractRoutingDataSource 的determineCurrentLookupKey方法,去读取当前线程的ThreadLocal,获取不到,返回resolvedDefaultDataSource即主数据源,从而查询操作的是主数据库。
java
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;//默认数据源
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("xx");
} 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);
}
}
}
@Nullable
protected abstract Object determineCurrentLookupKey();
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}
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) {
//xx
} else {
return dataSource;
}
}
}
4.页面添加数据源并测试连接。创建控制层.
java
//=========================分析2
@PostMapping("/testConneciton")
@ResponseBody
public AjaxResult testConnection(MyDatasourceTable myDatasourceTable) {
putDataSource(myDatasourceTable);
DynamicDataSourceContextHolder.setDataSourceType(myDatasourceTable.getDatasourceName());
try {
int reult = serviceImpl.selectxxx();//做数据库查询
if (reult > 0) {
return AjaxResult.success("连接成功");
} else {
return AjaxResult.warn("连接失败");
}
} catch (Exception e) {
return AjaxResult.error("连接失败或用户名密码错误");
}finally{
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
private void putDataSource(MyDatasourceTable myDatasourceTable) {
String datasourceUrl = "";
if ("mysql".equals(myDatasourceTable.getDatasourceType())) {
myDatasourceTable.setDatasourceDrive("com.mysql.cj.jdbc.Driver");
datasourceUrl = "jdbc:mysql://" +
myDatasourceTable.getDatasourceIp() + ":" + String.valueOf(myDatasourceTable.getDatasourcePort().intValue()) + "/" +
myDatasourceTable.getDatasourceInstantname() + "?" + "useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&allowMultiQueries=true";
}
myDatasourceTable.setDatasourceUrl(datasourceUrl);
String username = myDatasourceTable.getDatasourceUsername();//数据库用户名
String password = myDatasourceTable.getDatasourcePassword();//密码
String url = datasourceUrl;//连接地址
String driverClassName = myDatasourceTable.getDatasourceDrive();//驱动名称
String dsType = myDatasourceTable.getDatasourceName();//数据名称
dynamicDataSource.addDataSource(username, password,
url, driverClassName, dsType);
}
分析2 :
putDataSource方法,将新增数据源保存到加载的数据源targetDataSources中,同时重新设置 resolvedDataSources。
DynamicDataSourceContextHolder.setDataSourceType(myDatasourceTable.getDatasourceName());此方法给当前线程的ThreadLocal中保存新增的数据源名称A(唯一标识),可以理解成切换了数据源。在执行查询数据源操作时,要获取连接,调用AbstractRoutingDataSource 路由的getConnection()执行determineTargetDataSource()方法,进一步的this.determineCurrentLookupKey()返回当前线程ThreadLocal保存的数据源名称,所以resolvedDataSources返回了新增数据源A,从而查询操作的是数据源A。查询操作完成后, DynamicDataSourceContextHolder.clearDataSourceType(); 清除ThreadLocal保存的数据源名称A(唯一标识)。
当业务再次触发数据库查询操作的时候,就回到分析1中,获取不到当前线程的ThreadLocal即返回主数据源,操作主数据源。
总结:
1.DynamicDataSourceContextHolder.setDataSourceType切换数据源,
DynamicDataSourceContextHolder.clearDataSourceType()清除,这两个方法需要搭配使用,将清除ThreadLocal数据放入fianlly中执行,确保本次切换成功切回。
2.使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。