SpringBoot程序运行时修改主数据库配置(不需要改配置,不需要重启)
- 搞事背景
- 心路历程
搞事背景
在面试某家单位的时候,碰到了一家单位线上考试,要求开发一个springboot后台。一眼看去都是正常的需求,突然我在里面发现了一个奇葩要求,要求数据库允许线上修改,并且不能通过修改配置文件的方式。可以简单理解成用rest请求也能修改数据库配置。说实话,这成功引起了我的注意,像这种网上千篇一律回答只有多数据源配置和数据库配置数据源的解决方案,我决定来找点刺激,最终成功在半小时内整了出来。
大家可以收藏下,我面试能碰到,你们也可以,嘿嘿嘿
心路历程
- spring基操就是IOC,那就是说我的数据源dataSource大概率只有一个实例,如下图
- 明显没有用到池的概念,也就不会动态生成新的数据源,并且这种配置文件数据大都是直接注入的,可以通过getBean方法获取到dataSource并且通过反射修改其中的参数属性达到修改配置的目的,然后我就顺利找到了数据库配置的位置,如下图。
反射修改代码如下
DynamicDataSource dataSource = SpringUtils.getBean("dynamicDataSource");
Field field = dataSource.getClass().getSuperclass().getDeclaredField("targetDataSources");
field.setAccessible(true);
Map map = (HashMap) field.get(dataSource);
DruidDataSourceWrapper wrapper = (DruidDataSourceWrapper)map.get(DataSourceType.MASTER.toString());
Field jdbcUrl =wrapper.getClass().getSuperclass().getSuperclass().getDeclaredField("jdbcUrl");
Field username =wrapper.getClass().getSuperclass().getSuperclass().getDeclaredField("username");
Field password =wrapper.getClass().getSuperclass().getSuperclass().getDeclaredField("password");
jdbcUrl.setAccessible(true);
username.setAccessible(true);
password.setAccessible(true);
ReflectUtils.setFieldValue(wrapper, "jdbcUrl", vo.getHost());
ReflectUtils.setFieldValue(wrapper, "username", vo.getUsername());
ReflectUtils.setFieldValue(wrapper, "password", vo.getPassword());
jdbcUrl.setAccessible(false);
username.setAccessible(false);
password.setAccessible(false);
map.put(DataSourceType.MASTER.toString(), wrapper);
ReflectUtils.setFieldValue(dataSource, "targetDataSources", map);
field.setAccessible(false);
-
这时候我发现,调接口修改配置之后再去查询,已经成功修改了数据源,在我很满意的时候,我手欠先点了查询,再去改配置,再去查询时候,果不其然数据库没切换过来,很容易就能想到这是数据库在建立连接之后,数据库已经初始化了,我再去修改配置在短时间内再去查询会继续沿用之前初始化的配置,聪明的我,就马上去找数据源各个父级里面初始化方法,结果还真让我找到了,在DruidDataSource这个类下,如下图
-
果然在这里可以看到有个inited参数在把关着初始化配置的读取,但是可千万别急着改inited属性,这种大工程的数据化初始肯定会设计到一堆线程池的调度,改一个inited肯定没什么卵用,
我改过了,成功报了一堆错,但是好巧不巧给我发现了一个restart方法,不得不说数据库开发人员给自己留的后路真多,如下图
-
果然里面有一大票的参数初始化和重置操作,
基操基操,那后面就很简单了,我们直接在反射里面调这个方法就能完成数据源初始化了吧,然后我兴冲冲的把代码写好了,如下
DynamicDataSource dataSource = SpringUtils.getBean("dynamicDataSource");
Field field = dataSource.getClass().getSuperclass().getDeclaredField("targetDataSources");
field.setAccessible(true);
Map map = (HashMap) field.get(dataSource);
DruidDataSourceWrapper wrapper = (DruidDataSourceWrapper)map.get(DataSourceType.MASTER.toString());
// 数据源的配置存放点
Field jdbcUrl =wrapper.getClass().getSuperclass().getSuperclass().getDeclaredField("jdbcUrl");
Field username =wrapper.getClass().getSuperclass().getSuperclass().getDeclaredField("username");
Field password =wrapper.getClass().getSuperclass().getSuperclass().getDeclaredField("password");
// 重启方法
Method restartMethod = wrapper.getClass().getSuperclass().getDeclaredMethod("restart");
jdbcUrl.setAccessible(true);
username.setAccessible(true);
password.setAccessible(true);
ReflectUtils.setFieldValue(wrapper, "jdbcUrl", vo.getHost());
ReflectUtils.setFieldValue(wrapper, "username", vo.getUsername());
ReflectUtils.setFieldValue(wrapper, "password", vo.getPassword());
jdbcUrl.setAccessible(false);
username.setAccessible(false);
password.setAccessible(false);
map.put(DataSourceType.MASTER.toString(), wrapper);
ReflectUtils.setFieldValue(dataSource, "targetDataSources", map);
field.setAccessible(false);
// 重启重启重启
restartMethod.invoke(wrapper);
- 到这里,不得不佩服数据库开发人员的伟大,给我们留了这么便捷的一个方法,然后,不出意外的就是给我当头一棒,重启后还是报错,好消息是已经能跑到执行sql那一步了,坏消息是sql连接没了,debug时候异常点附近获取connections的数组里面空空如也,这个问题我后面居然还无法复现了,图都没得截,所以到这里就成功的兄弟们一定要往下看,经过五分钟大眼瞪小眼盯着流程里面每一步的参数变化时,我发现有个属性叫poolingCount,我重置之后在查询,系统调用init()方法后,这个属性的值居然没有清,然后被顺理成章的判断成当前池里面连接数量足够,所以没有建立连接,源码逻辑如下
- 后面我去restart()方法里挨个找了,这个参数果然没清,
Druid现存bug加1,也不知道新版本有没有修复,反正这也不是留给我们用的方法,哈哈哈,那结果显而易见了,只要把poolingCount通过反射整个归零,就ok了,最终版代码如下
DynamicDataSource dataSource = SpringUtils.getBean("dynamicDataSource");
Field field = dataSource.getClass().getSuperclass().getDeclaredField("targetDataSources");
field.setAccessible(true);
Map map = (HashMap) field.get(dataSource);
DruidDataSourceWrapper wrapper = (DruidDataSourceWrapper)map.get(DataSourceType.MASTER.toString());
// 数据源的配置存放点
Field jdbcUrl =wrapper.getClass().getSuperclass().getSuperclass().getDeclaredField("jdbcUrl");
Field username =wrapper.getClass().getSuperclass().getSuperclass().getDeclaredField("username");
Field password =wrapper.getClass().getSuperclass().getSuperclass().getDeclaredField("password");
// 重启方法
Method restartMethod = wrapper.getClass().getSuperclass().getDeclaredMethod("restart");
Field poolingCount = wrapper.getClass().getSuperclass().getDeclaredField("poolingCount");
jdbcUrl.setAccessible(true);
username.setAccessible(true);
password.setAccessible(true);
poolingCount.setAccessible(true);
ReflectUtils.setFieldValue(wrapper, "jdbcUrl", vo.getHost());
ReflectUtils.setFieldValue(wrapper, "username", vo.getUsername());
ReflectUtils.setFieldValue(wrapper, "password", vo.getPassword());
// 一定要清,关系到数据库连接的新建,restart方法没有自动清0,导致restart之后查询获取不到线程
ReflectUtils.setFieldValue(wrapper, "poolingCount", 0);
jdbcUrl.setAccessible(false);
username.setAccessible(false);
password.setAccessible(false);
poolingCount.setAccessible(false);
map.put(DataSourceType.MASTER.toString(), wrapper);
ReflectUtils.setFieldValue(dataSource, "targetDataSources", map);
field.setAccessible(false);
// 重启重启重启
restartMethod.invoke(wrapper);
- 最后测试一下,查询、换数据库、查询,果然可以立马切换了,效果如下。
- 最后总结一下,java开发当的越久,对IOC的厉害之处和便捷之处就越发佩服,虽然现在已经有很多其他主流的开发语言了,但是spring这种特性还是怎么看怎么牛皮,在能用rest接口改数据库配置后,其他就不难想到,很多热部署、线上配置直接生效等等原理其实大同小异,而框架开发、脚手架开发其实离我们自己也并没那么遥远。
- 题外话:可能有兄弟会说以前建立的线程怎么办,为啥不清呢,对此,我只想说,我只想等它安安静静的过期然后自取灭亡