SpringBoot程序运行时动态修改主数据库配置(不需要改配置,不需要重启)

news2024/12/23 6:27:21

SpringBoot程序运行时修改主数据库配置(不需要改配置,不需要重启)

  • 搞事背景
  • 心路历程

搞事背景

在面试某家单位的时候,碰到了一家单位线上考试,要求开发一个springboot后台。一眼看去都是正常的需求,突然我在里面发现了一个奇葩要求,要求数据库允许线上修改,并且不能通过修改配置文件的方式。可以简单理解成用rest请求也能修改数据库配置。说实话,这成功引起了我的注意,像这种网上千篇一律回答只有多数据源配置和数据库配置数据源的解决方案,我决定来找点刺激,最终成功在半小时内整了出来。

大家可以收藏下,我面试能碰到,你们也可以,嘿嘿嘿

心路历程

  1. spring基操就是IOC,那就是说我的数据源dataSource大概率只有一个实例,如下图
    在这里插入图片描述
  2. 明显没有用到池的概念,也就不会动态生成新的数据源,并且这种配置文件数据大都是直接注入的,可以通过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);
  1. 这时候我发现,调接口修改配置之后再去查询,已经成功修改了数据源,在我很满意的时候,我手欠先点了查询,再去改配置,再去查询时候,果不其然数据库没切换过来,很容易就能想到这是数据库在建立连接之后,数据库已经初始化了,我再去修改配置在短时间内再去查询会继续沿用之前初始化的配置,聪明的我,就马上去找数据源各个父级里面初始化方法,结果还真让我找到了,在DruidDataSource这个类下,如下图
    在这里插入图片描述

  2. 果然在这里可以看到有个inited参数在把关着初始化配置的读取,但是可千万别急着改inited属性,这种大工程的数据化初始肯定会设计到一堆线程池的调度,改一个inited肯定没什么卵用,我改过了,成功报了一堆错,但是好巧不巧给我发现了一个restart方法,不得不说数据库开发人员给自己留的后路真多,如下图
    在这里插入图片描述

  3. 果然里面有一大票的参数初始化和重置操作,基操基操,那后面就很简单了,我们直接在反射里面调这个方法就能完成数据源初始化了吧,然后我兴冲冲的把代码写好了,如下

		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);
  1. 到这里,不得不佩服数据库开发人员的伟大,给我们留了这么便捷的一个方法,然后,不出意外的就是给我当头一棒,重启后还是报错,好消息是已经能跑到执行sql那一步了,坏消息是sql连接没了,debug时候异常点附近获取connections的数组里面空空如也,这个问题我后面居然还无法复现了,图都没得截,所以到这里就成功的兄弟们一定要往下看,经过五分钟大眼瞪小眼盯着流程里面每一步的参数变化时,我发现有个属性叫poolingCount我重置之后在查询,系统调用init()方法后,这个属性的值居然没有清,然后被顺理成章的判断成当前池里面连接数量足够,所以没有建立连接,源码逻辑如下
    在这里插入图片描述
  2. 后面我去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);
  1. 最后测试一下,查询、换数据库、查询,果然可以立马切换了,效果如下。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. 最后总结一下,java开发当的越久,对IOC的厉害之处和便捷之处就越发佩服,虽然现在已经有很多其他主流的开发语言了,但是spring这种特性还是怎么看怎么牛皮,在能用rest接口改数据库配置后,其他就不难想到,很多热部署、线上配置直接生效等等原理其实大同小异,而框架开发、脚手架开发其实离我们自己也并没那么遥远。
  3. 题外话:可能有兄弟会说以前建立的线程怎么办,为啥不清呢,对此,我只想说,我只想等它安安静静的过期然后自取灭亡

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/422553.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Raft: 基于 Log 复制的共识算法

References Raft 演示 In Search of an Understandable Consensus Algorithm (Extended Version) 1. Raft 是什么 1.1 目标: 复制 Log 在讲解 Raft 协议的具体行为之前我们需要明白 Raft 的目标是什么?在一些情况下我们需要保证分布式集群中的机器拥有相同的数…

IOC容器——Bean

IOC容器——BeanBean配置name别名属性Bean作用范围scopeBean的实例化构造方法示例化静态工厂实例化实例工厂与FactoryBean实例工厂FactoryBeanbean的生命周期Bean配置 name别名属性 Bean ID 唯一,而关于Spring别名,我们可以在配置文件中使用name来定义&…

Google Play管理中心和ASO的重要性

Android Vitals 是我们应用优化的重要组成部分,能够显示应用的运行状况。一般来说,如果应用具有良好的体验,它会更容易在Google Play中被用户发现,从而获得更好的排名和更多的安装量。 从开发者的角度来看,Android Vi…

JAVA8新特性stream流收集为Map,value为null导致空指针的问题

jdk8 新特性stream深受喜爱&#xff0c;平时使用比较多&#xff0c;其中有&#xff1a; Map<String, String> collect2 list.stream().collect(Collectors.toMap(Book::getName, Book::getIdNO,(pre, after) -> pre)); 现象如下&#xff1a; package MainTest.str…

HTML5 <nav> 标签、HTML5 <noscript> 标签

HTML5 <nav> 标签 实例 HTML5 <nav>标签用于表示HTML页面中的导航&#xff0c;可以是页与页之间导航&#xff0c;也可以是页内的段与段之间导航。 一个导航链接实例&#xff1a; <nav> <a href"/html/">HTML</a> | <a href&qu…

关于pinduoduo开放接口测试

什么是接口测试 接口测试是测试系统组件间接口的一种方式&#xff0c;接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是检查数据的增删改查操作&#xff0c;以及系统之间的逻辑关系等。 接口测试作为集成测试的一部分&#xff0c;通过直接…

归并排序(非递归实现) 计数排序

上一期我们说了归并排序的递归是如何实现的&#xff0c;但是递归如果层次太多的话容易栈溢出&#xff0c;所以我们还需要掌握非递归的实现&#xff0c;但是我们非递归需要如何实现&#xff1f; 下面我们就来看一下非递归的实现 归并排序的非递归实现他并不需要栈队列这些东西…

No.042<软考>《(高项)备考大全》【第26章】法律法规(合同法、招投标法、政府采购法、著作权法)

【第26章】法律法规&#xff08;合同法、招投标法、政府采购法、著作权法&#xff09;1 考试相关2 合同法练习题参考答案3 招投标法3.1 法规时间总结3.2 招投标流程3.3 招标3.4 投标3.5 评标3.6 练习题参考答案3.7 论文写作3.8 投标文件的编写应该注意哪些事项4 著作权法4.1 练…

找漏洞赚外快?给ChatGPT挑毛病,最高奖励14万

反正闲着也是闲着&#xff0c;不如来给ChatGPT找漏洞&#xff1f;毕竟&#xff0c;万一真的找到漏洞了还能赚一笔外快。 当地时间 4 月 11 日&#xff0c;OpenAI 宣布推出漏洞赏金计划。该公司将根据报告问题的严重性和影响提供现金奖励&#xff0c;奖励范围从 200 美元到 200…

Spring经典扩展接口应用:BeanPostProcessor

备注&#xff1a;新进行基本思路总结&#xff0c;四五月总结完 一、BeanPostProcessor基本知识总结 BeanPostProcessor是Bean级处理器&#xff0c;用于在bean实例化后、初始化后自定义修改bean实例&#xff0c;如属性校验、针对自定义bean做统一处理等。 BeanPostProcessor接…

实战:向人工智能看齐用Docker部署一个ChatGPT

文章目录前言鉴赏chatgpt环境要求开始搭建云安装docker从docker仓库拉取chatgpt-web镜像创建容器并运行chatgpt-web创建容器启动chatgpt-web访问自己的chatgpt总结前言 目前GPT-4都官宣步入多模态大型语言模型领域了&#xff0c;大佬竟然还没有体验GPT么。作为一个资深搬砖人士…

容器编排部署

一、概述 容器编排部署的作用&#xff1a; 实现复杂容器应用架构之间的互联&#xff0c;减少大量容器部署的成本 Docker"三剑客"编排部署 工具︰ docker machine 用于创建和管理docker host docker compose 通过一个文件定义复杂的容器应用之间的关系 容器与容…

【FMCW系统性能参数之测量精度公式推导】

本文编辑&#xff1a;调皮哥的小助理 连续多篇文章都在说FMCW雷达系统性能参数这个事儿&#xff0c;如&#xff1a; &#xff08;1&#xff09;从奈奎斯特采样定理推导FMCW雷达系统性能参数 &#xff08;2&#xff09;从FMCW毫米波雷达系统的性能参数理解4D成像毫米波雷达的设…

深度学习语义分割篇——FCN原理详解篇

&#x1f34a;作者简介&#xff1a;秃头小苏&#xff0c;致力于用最通俗的语言描述问题 &#x1f34a;往期回顾&#xff1a;目标检测系列——开山之作RCNN原理详解    目标检测系列——Fast R-CNN原理详解    目标检测系列——Faster R-CNN原理详解 &#x1f34a;近期目标&…

Unity --- 3d数学 --- 坐标系统

1.世界坐标系是固定不动的 2.每一个游戏物体在世界坐标系中都有对应的坐标和方向 1.轴心点的位置不是固定的&#xff0c;是可以人为设定的 1.Screen Space --- 屏幕坐标 2.我们看到的屏幕其实就是相机所在的平面的位置 --- 而屏幕坐标系的Z其实就是游戏中的物体到相机平面的…

GDOUCTF

WEB hate eat snake 这是一个JS的题目&#xff0c;但是这个题目好像有点奇怪&#xff0c;不是很理解&#xff0c;当时我找到了我寝室JS的大哥&#xff0c;跟大哥说了一下我的思路&#xff0c;就是他根据这个time然后/1000转化为秒&#xff0c;就当作是我们玩游戏的一个分数&a…

速卖通韩国下载量再次登顶,7500万投资换来回报

韩国市场&#xff0c;还是一片蓝海。 速卖通终于等到了回报。 近日&#xff0c;数据平台 Data ai 显示&#xff0c;3 月 9 日以来&#xff0c;速卖通再次成为韩国购物 App 下载量第一名&#xff0c;超过当地电商平台 Coupang。 这或许和速卖通近日在韩国的布局有关。 前些天…

使用File System Access API 让浏览器可以操作文件

使用File System Access API 让浏览器可以操作文件 在早期我们开始学习前端三件套时&#xff0c;经常会听到这样的说法&#xff1a;浏览器是一个沙盒&#xff0c;它不允许我们操作本地文件&#xff0c;但是现在这个说法已经不再适用了&#xff0c;因为我们可以使用 File Syste…

2023接口自动化测试,完整入门篇

1. 什么是接口测试 顾名思义&#xff0c;接口测试是对系统或组件之间的接口进行测试&#xff0c;主要是校验数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及相互逻辑依赖关系。其中接口协议分为HTTP,WebService,Dubbo,Thrift,Socket等类型&#xff0c;测试类型又主…

BBR算法

BBR算法 简述 bbr算法为google在2016年提出&#xff0c;用于改善tcp的性能&#xff0c;提升稳定性&#xff0c;降低延迟&#xff0c;更好地应对网络损伤。在整个算法调节周期中&#xff0c;bbr算法都在尽力维持最大bw和最小rtt。 对比传统的tcp算法 传统算法不能区分是拥塞导…