1. 引言
无论是多套源还是动态数据源,相对来说还是固定的数据源(如一主一从,一主多从等),即在编码时已经确定的数据库数量,只是在具体使用哪一个时进行动态处理。如果数据源本身并不确定,或者说需要根据用户输入来连接数据库,这时,如何处理呢?可以想象现在我们有一个需求,需要对数据库进行连接管理,用户可以输入对应的数据库连接信息,然后可以查看数据库有哪些表。这就跟平时使用的数据库管理软件有点类似了,如 MySQL Workbench、Navicat、SQLyog,下图是 SQLyog 截图:
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 流程说明
- 用户输入数据库连接参数(包括 IP、端口、驱动名、数据库名、用户名、密码)
- 根据数据库连接参数创建数据源
- 添加数据源到动态数据源中
- 切换数据源
- 操作数据库
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页)
初级—中级—高级三个级别的大厂面试真题
阿里云——Java 实习生/初级
List 和 Set 的区别 HashSet 是如何保证不重复的
HashMap 是线程安全的吗,为什么不是线程安全的(最好画图说明多线程环境下不安全)?
HashMap 的扩容过程
HashMap 1.7 与 1.8 的 区别,说明 1.8 做了哪些优化,如何优化的?
对象的四种引用
Java 获取反射的三种方法
Java 反射机制
Arrays.sort 和 Collections.sort 实现原理 和区别
Cloneable 接口实现原理
异常分类以及处理机制
wait 和 sleep 的区别
数组在内存中如何分配
答案展示:
美团——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 内部执行流程
答案展示:
蚂蚁金服——Java 高级
题 1:
- jdk1.7 到 jdk1.8 Map 发生了什么变化(底层)?
- ConcurrentHashMap
- 并行跟并发有什么区别?
- jdk1.7 到 jdk1.8 java 虚拟机发生了什么变化?
- 如果叫你自己设计一个中间件,你会如何设计?
- 什么是中间件?
- ThreadLock 用过没有,说说它的作用?
- Hashcode()和 equals()和==区别?
- mysql 数据库中,什么情况下设置了索引但无法使用?
- mysql 优化会不会,mycat 分库,垂直分库,水平分库?
- 分布式事务解决方案?
- sql 语句优化会不会,说出你知道的?
- mysql 的存储引擎了解过没有?
- 红黑树原理?
题 2:
- 说说三种分布式锁?
- redis 的实现原理?
- redis 数据结构,使⽤场景?
- redis 集群有哪⼏种?
- codis 原理?
- 是否熟悉⾦融业务?记账业务?蚂蚁⾦服对这部分有要求。
好啦~展示完毕,大概估摸一下自己是青铜还是王者呢?
前段时间,在和群友聊天时,把今年他们见到的一些不同类别的面试题整理了一番,于是有了以下面试题集,也一起分享给大家~
基础篇
JVM 篇
MySQL 篇
Redis 篇
由于篇幅限制,详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
需要的小伙伴,可以一键三连,下方获取免费领取方式!