1. 引言

无论是多套源还是动态数据源,相对来说还是固定的数据源(如一主一从,一主多从等),即在编码时已经确定的数据库数量,只是在具体使用哪一个时进行动态处理。如果数据源本身并不确定,或者说需要根据用户输入来连接数据库,这时,如何处理呢?可以想象现在我们有一个需求,需要对数据库进行连接管理,用户可以输入对应的数据库连接信息,然后可以查看数据库有哪些表。这就跟平时使用的数据库管理软件有点类似了,如 MySQL Workbench、Navicat、SQLyog,下图是 SQLyog 截图:

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_java

SQLyog

本文基于前面的示例,添加一个功能,根据用户输入的数据库连接信息,连接数据库,并返回数据库的表信息。内容包括动态添加数据源、动态代理简化数据源操作等。

本文所涉及到的示例代码[1]:​​https://github.com/mianshenglee/my-example/tree/master/multi-datasource​​,读者可结合一起看。

2. 参数化变更源说明

2.1 解决思路

Spring Boot 的动态数据源,本质上是把多个数据源存储在一个 Map 中,当需要使用某个数据源时,从 Map 中获取此数据源进行处理。在动态数据源处理时,通过继承抽象类 ​​AbstractRoutingDataSource​​ 可实现此功能。既然是 Map ,如果有新的数据源,把新的数据源添加到此 Map 中就可以了。这就是整个解决思路。

但是,查看 ​​AbstractRoutingDataSource​​​ 源码,可以发现,存放数据源的 Map ​​targetDataSources​​​ 是 private 的,而且并没有提供对此 Map 本身的操作,它提供的是两个关键操作:​​setTargetDataSources​​​ 及 ​​afterPropertiesSet​​​ 。其中 ​​setTargetDataSources​​​ 设置整个 Map 目标数据源,​​afterPropertiesSet​​​ 则是对 Map 目标数据源进行解析,形成最终使用的 ​​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);
});

因此,为实现动态添加数据源到 Map 的功能,我们可以根据这两个关键操作进行处理。

2.2 流程说明

  1. 用户输入数据库连接参数(包括 IP、端口、驱动名、数据库名、用户名、密码)
  2. 根据数据库连接参数创建数据源
  3. 添加数据源到动态数据源中
  4. 切换数据源
  5. 操作数据库

3. 实现参数化变更源


说明,下面的操作基于之前文章的示例,基本的工程搭建及配置不再重复说明,有需要可参考文章。


3.1 改造动态数据源

3.1.1 动态数据源添加功能

为了可以动态添加数据源到 Map ,我们需要对动态数据源进行改造。如下:

public class DynamicDataSource extends AbstractRoutingDataSource {  
private Map<Object, Object> backupTargetDataSources;

/**
* 自定义构造函数
*/
public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSource){
backupTargetDataSources = targetDataSource;
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(backupTargetDataSources);
super.afterPropertiesSet();
}

/**
* 添加新数据源
*/
public void addDataSource(String key, DataSource dataSource){
this.backupTargetDataSources.put(key,dataSource);
super.setTargetDataSources(this.backupTargetDataSources);
super.afterPropertiesSet();
}

@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getContextKey();
}
}


  • 添加了自定义的 ​​backupTargetDataSources​​​ 作为原 ​​targetDataSources​​ 的拷贝
  • 自定义构造函数,把需要保存的目标数据源拷贝到自定义的 Map 中
  • 添加新数据源时,依然使用 ​​setTargetDataSources​​​ 及 ​​afterPropertiesSet​​ 完成新数据源添加。
  • 注意:​​afterPropertiesSet​​​ 的作用很重要,它负责解析成可用的目标数据源。


3.1.2 动态数据源配置

原来在创建动态数据源时,使用的是无参数构造函数,经过前面改造后,使用有参构造函数,如下:

@Bean  
@Primary
public DataSource dynamicDataSource() {
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource());
dataSourceMap.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource());
//有参构造函数
return new DynamicDataSource(masterDataSource(), dataSourceMap);
}

3.2 添加数据源工具类

3.2.1 Spring 上下文工具类

在 Spring Boot 使用过程中,经常会用到 Spring 的上下文,常见的就是从 Spring 的 IOC 中获取 bean 来进行操作。由于 Spring 使用的 IOC 基本上把 bean 都注入到容器中,因此需要 Spring 上下文来获取。我们在 context 包下添加 ​​SpringContextHolder​​ ,如下:

@Component  
public class SpringContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextHolder.applicationContext = applicationContext;
}
/**
* 返回上下文
*/
public static ApplicationContext getContext(){
return SpringContextHolder.applicationContext;
}
}

通过 ​​getContext​​ 就可以获取上下文,进而操作。

3.2.2 数据源操作工具

通过参数添加数据源,需要根据参数构造数据源,然后添加到前面说的 Map 中。如下:

public class DataSourceUtil {  
/**
* 创建新的数据源,注意:此处只针对 MySQL 数据库
*/
public static DataSource makeNewDataSource(DbInfo dbInfo){
String url = "jdbc:mysql://"+dbInfo.getIp() + ":"+dbInfo.getPort()+"/"+dbInfo.getDbName()
+"?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8";
String driveClassName = StringUtils.isEmpty(dbInfo.getDriveClassName())? "com.mysql.cj.jdbc.Driver":dbInfo.getDriveClassName();
return DataSourceBuilder.create().url(url)
.driverClassName(driveClassName)
.username(dbInfo.getUsername())
.password(dbInfo.getPassword())
.build();
}

/**
* 添加数据源到动态源中
*/
public static void addDataSourceToDynamic(String key, DataSource dataSource){
DynamicDataSource dynamicDataSource = SpringContextHolder.getContext().getBean(DynamicDataSource.class);
dynamicDataSource.addDataSource(key,dataSource);
}

/**
* 根据数据库连接信息添加数据源到动态源中
* @param key
* @param dbInfo
*/
public static void addDataSourceToDynamic(String key, DbInfo dbInfo){
DataSource dataSource = makeNewDataSource(dbInfo);
addDataSourceToDynamic(key,dataSource);
}
}


  • 通过 ​​DataSourceBuilder​​ 及相应的参数来构造数据源,注意此处只针对 MySQL 作处理,其它数据库的话,对应的 url 及 DriveClassName 需作相应的变更。
  • 添加数据源时,通过 Spring 上下文获取动态数据源的 bean,然后添加。


3.3 使用参数变更数据源

前面两步已实现添加数据源,下面我们根据需求(根据用户输入的数据库连接信息,连接数据库,并返回数据库的表信息),看看如何使用它。

3.3.1 添加查询数据库表信息的 Mapper

通过 MySQL 的 ​​information_schema​​ 可以获取表信息。

@Repository  
public interface TableMapper extends BaseMapper<TestUser> {
/**
* 查询表信息
*/
@Select("select table_name, table_comment, create_time, update_time " +
" from information_schema.tables " +
" where table_schema = (select database())")
List<Map<String,Object>> selectTableList();
}

3.3.2 定义数据库连接信息对象

把数据库连接信息通过一个类进行封装。

@Data  
public class DbInfo {
private String ip;
private String port;
private String dbName;
private String driveClassName;
private String username;
private String password;
}

3.3.3 参数化变更源并查询表信息

在 controller 层,我们定义一个查询表信息的接口,根据传入的参数,连接数据源,返回表信息:

/**  
* 根据数据库连接信息获取表信息
*/
@GetMapping("table")
public Object findWithDbInfo(DbInfo dbInfo) throws Exception {
//数据源key
String newDsKey = System.currentTimeMillis()+"";
//添加数据源
DataSourceUtil.addDataSourceToDynamic(newDsKey,dbInfo);
DynamicDataSourceContextHolder.setContextKey(newDsKey);
//查询表信息
List<Map<String, Object>> tables = tableMapper.selectTableList();
DynamicDataSourceContextHolder.removeContextKey();
return ResponseResult.success(tables);
}


  • 访问地址 ​​ ,对应数据库连接参数。
  • 此处数据源的 key 是无意义的,建议根据实际场景设置有意义的值


4. 动态代理消除模板代码

前面已经完成了参数化切换数据源功能,但还有一点就是有模板代码,如添加数据源、切换数据源、对此数据源进行 CURD 操作、释放数据源,如果每个地方都这样做,就很繁琐,这个时候,就需要用到动态代理了。此处,使用 JDK 自带的动态代理,实现参数化变更数据源的功能,消除模板代码。

4.1 添加 JDK 动态代理

添加 proxy 包,添加 ​​JdkParamDsMethodProxy​​​ 类,实现 ​​InvocationHandler​​​ 接口,在 ​​invoke​​ 中编写参数化切换数据源的逻辑即可。如下:

public class JdkParamDsMethodProxy implements InvocationHandler {  
// 代理对象及相应参数
private String dataSourceKey;
private DbInfo dbInfo;
private Object targetObject;
public JdkParamDsMethodProxy(Object targetObject, String dataSourceKey, DbInfo dbInfo) {
this.targetObject = targetObject;
this.dataSourceKey = dataSourceKey;
this.dbInfo = dbInfo;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//切换数据源
DataSourceUtil.addDataSourceToDynamic(dataSourceKey, dbInfo);
DynamicDataSourceContextHolder.setContextKey(dataSourceKey);
//调用方法
Object result = method.invoke(targetObject, args);
DynamicDataSourceContextHolder.removeContextKey();
return result;
}

/**
* 创建代理
*/
public static Object createProxyInstance(Object targetObject, String dataSourceKey, DbInfo dbInfo) throws Exception {
return Proxy.newProxyInstance(targetObject.getClass().getClassLoader()
, targetObject.getClass().getInterfaces(), new JdkParamDsMethodProxy(targetObject, dataSourceKey, dbInfo));
}
}


  • 代码中,需要使用的参数通过构造函数传入
  • 通过 ​​Proxy.newProxyInstance​​​ 创建代理,在方法执行时( ​​invoke​​​ ) 进行数据源添加、切换、数据库操作、清除等


4.2 使用代理实现功能

有了代理,在添加和切换数据源时就可以擦除模板代码,前面的业务代码就变成:

@GetMapping("table")  
public Object findWithDbInfo(DbInfo dbInfo) throws Exception {
//数据源key
String newDsKey = System.currentTimeMillis()+"";
//使用代理切换数据源
TableMapper tableMapperProxy = (TableMapper)JdkParamDsMethodProxy.createProxyInstance(tableMapper, newDsKey, dbInfo);
List<Map<String, Object>> tables = tableMapperProxy.selectTableList();
return ResponseResult.success(tables);
}

通过代理,代码就简洁多了。

5. 总结

一直想整理出一份完美的面试宝典,但是时间上一直腾不开,这套一千多道面试题宝典,结合今年金三银四各种大厂面试题,以及 GitHub 上 star 数超 30K+ 的文档整理出来的,我上传以后,毫无意外的短短半个小时点赞量就达到了 13k,说实话还是有点不可思议的。

一千道互联网 Java 工程师面试题

内容涵盖:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、MySQL、Spring、SpringBoot、SpringCloud、RabbitMQ、Kafka、Linux等技术栈(485页)

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_spring_02

初级—中级—高级三个级别的大厂面试真题

阿里云——Java 实习生/初级


List 和 Set 的区别 HashSet 是如何保证不重复的

HashMap 是线程安全的吗,为什么不是线程安全的(最好画图说明多线程环境下不安全)?

HashMap 的扩容过程

HashMap 1.7 与 1.8 的 区别,说明 1.8 做了哪些优化,如何优化的?

对象的四种引用

Java 获取反射的三种方法

Java 反射机制

Arrays.sort 和 Collections.sort 实现原理 和区别

Cloneable 接口实现原理

异常分类以及处理机制

wait 和 sleep 的区别

数组在内存中如何分配


答案展示:

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_java_03

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_spring_04

美团——Java 中级


BeanFactory 和 ApplicationContext 有什么区别

Spring Bean 的生命周期

Spring IOC 如何实现

说说 Spring AOP

Spring AOP 实现原理

动态代理(cglib 与 JDK)

Spring 事务实现方式

Spring 事务底层原理

如何自定义注解实现功能

Spring MVC 运行流程

Spring MVC 启动流程

Spring 的单例实现原理

Spring 框架中用到了哪些设计模式

为什么选择 Netty

说说业务中,Netty 的使用场景

原生的 NIO 在 JDK 1.7 版本存在 epoll bug

什么是 TCP 粘包/拆包

TCP 粘包/拆包的解决办法

Netty 线程模型

说说 Netty 的零拷贝

Netty 内部执行流程


答案展示:

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_java_05

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_java_06

蚂蚁金服——Java 高级

题 1:


  1. jdk1.7 到 jdk1.8 Map 发生了什么变化(底层)?
  2. ConcurrentHashMap
  3. 并行跟并发有什么区别?
  4. jdk1.7 到 jdk1.8 java 虚拟机发生了什么变化?
  5. 如果叫你自己设计一个中间件,你会如何设计?
  6. 什么是中间件?
  7. ThreadLock 用过没有,说说它的作用?
  8. Hashcode()和 equals()和==区别?
  9. mysql 数据库中,什么情况下设置了索引但无法使用?
  10. mysql 优化会不会,mycat 分库,垂直分库,水平分库?
  11. 分布式事务解决方案?
  12. sql 语句优化会不会,说出你知道的?
  13. mysql 的存储引擎了解过没有?
  14. 红黑树原理?


题 2:


  1. 说说三种分布式锁?
  2. redis 的实现原理?
  3. redis 数据结构,使⽤场景?
  4. redis 集群有哪⼏种?
  5. codis 原理?
  6. 是否熟悉⾦融业务?记账业务?蚂蚁⾦服对这部分有要求。


好啦~展示完毕,大概估摸一下自己是青铜还是王者呢?

前段时间,在和群友聊天时,把今年他们见到的一些不同类别的面试题整理了一番,于是有了以下面试题集,也一起分享给大家~

基础篇

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_spring boot_07

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_编程语言_08

JVM 篇

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_编程语言_09

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_编程语言_10

MySQL 篇

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_编程语言_11

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_java_12

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_数据源_13

Redis 篇

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_spring boot_14

搞定SpringBoot多数据源:参数化变更源(我内容很“长”,你忍一下!)_java_15



由于篇幅限制,详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!


需要的小伙伴,可以一键三连,下方获取免费领取方式!