一个类实现Mybatis的SQL热更新

引言

平时用SpringBoot+Mybatis开发项目,如果项目比较大启动时间很长的话,每次修改Mybatis在Xml中的SQL就需要重启一次。假设项目重启一次需要5分钟,那修改10次SQL就过去了一个小时,成本有点太高了。关键是每次修改完代码之后再重启服务,我们的代码思路也会被中断,这样更会降低我们的开发效率。有没有一种方法可以让我们修改完SQL之后不用重启呢?答案是肯定的,我自己亲测有效。以后开发修改了SQL可以自动更新Mybatis的配置,如果是修改了Java代码可以使用idea自带的Hot Swap进行Class的Recompile,快捷键是CTRL+SHIFT+F9。你也可以装一个JRebel插件,这个插件同样只能更新Class不能更新Mybatis SQL。

先思考三个问题,文中会给出回答。

  • Mybatis动态SQL的实现原理是什么?
  • Mybatis是在什么时候读取的XML配置?
  • 读取的配置放在了哪里?

源码

Mybatis SQL 热更新的实现流程如下图。

话不多说,先上完整代码,只需要一个类即可实现,文末我会将代码拆解分析其原理。大家可以直接拿去项目上使用,记得上线的时候把热更新的开关关闭,以免影响线上性能。

package com.ITGuoGuo.springtemplate.config;import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;@Slf4j
@Component
public class MapperHotSwap {//@Value("${mybatis.mapper-locations}")//private String packageSerchPath;//@Autowired//private MybatisProperties mybatisProperties;@Autowiredprivate MybatisPlusProperties mybatisPlusProperties;@Autowiredprivate SqlSessionFactory sqlSessionFactory;private Resource[] mapperLocations;private Configuration config;private HashMap<String, Long> fileChange = new HashMap<String, Long>();// 记录文件是否变化@org.springframework.context.annotation.Configuration@ConfigurationProperties(prefix = MapperHotSwapProperties.PREFIX)@Datapublic static class MapperHotSwapProperties {public final static String PREFIX = "mybatis.mapper";private Boolean reload = false;}@Autowiredprivate MapperHotSwapProperties hotSwapProperties;@PostConstructpublic void init() {try {if (!hotSwapProperties.getReload()) return;prepareEnv();Runnable runnable = new Runnable() {public void run() {changeCompare();}};ScheduledExecutorService schedule = Executors.newSingleThreadScheduledExecutor();//首次执行1秒以后,定时执行时间间隔10秒schedule.scheduleAtFixedRate(runnable, 1, 10, TimeUnit.SECONDS);log.info("============Mybatis Mapper 热更新生效=============");} catch (Exception e) {log.error("包路径配置扫描错误", e);}}/*** 初始化 Mybatis Mapper 配置*/public void prepareEnv() throws Exception {this.config = sqlSessionFactory.getConfiguration();this.mapperLocations = new PathMatchingResourcePatternResolver().getResources(mybatisPlusProperties.getMapperLocations()[0]);for (Resource resource : mapperLocations) {// 文件内容帧值long lastFrame = resource.contentLength() + resource.lastModified();fileChange.put(resource.getFilename(), Long.valueOf(lastFrame));}}/*** xml文件已修改则重载配置;否则不处理*/public void changeCompare() {try {if (!isChanged()) return;// 清理removeConfig(config);// 重载for (Resource loc : mapperLocations) {try {XMLMapperBuilder builder = new XMLMapperBuilder(loc.getInputStream(), config, loc.toString(), config.getSqlFragments());builder.parse();} catch (IOException e) {log.error("mapper文件[" + loc.getFilename() + "]不存在或内容格式不对");}}log.info("------- mapper文件已全部更新 -------");} catch (Exception e) {log.error(e.getMessage(), e);}}/*** 判断文件是否变化*/boolean isChanged() throws IOException {boolean flag = false;for (Resource resource : mapperLocations) {String resourceName = resource.getFilename();Long lastFrame = fileChange.get(resourceName);long newFrame = resource.contentLength() + resource.lastModified();fileChange.put(resourceName, Long.valueOf(newFrame));// 新增或是修改,保存文件最新帧boolean addFlag = !fileChange.isEmpty() && !fileChange.containsKey(resourceName);boolean modifyFlag = null != lastFrame && lastFrame != newFrame;if (addFlag || modifyFlag) {flag = true;log.info("-------[" + resourceName + "]文件 已修改-------");}}return flag;}/*** 清空Configuration中几个重要的缓存*/private void removeConfig(Configuration configuration) throws Exception {Class<?> classConfig = configuration.getClass();clearMap(classConfig, configuration, "mappedStatements");clearMap(classConfig, configuration, "caches");clearMap(classConfig, configuration, "resultMaps");clearMap(classConfig, configuration, "parameterMaps");clearMap(classConfig, configuration, "keyGenerators");clearMap(classConfig, configuration, "sqlFragments");// 因为是使用的是Mybatis Plus,Mybatis Plus 使用的配置类是 Configuration 的子类 MybatisConfiguration。// 所以要去其父类 Configuration 中找 loadedResources 这个属性for (; Objects.nonNull(classConfig); classConfig = classConfig.getSuperclass()) {clearSet(classConfig, configuration, "loadedResources");}}private void clearMap(Class<?> classConfig, Configuration configuration, String fieldName) {Field field = getDeclaredField(classConfig, fieldName);if (Objects.isNull(field)) {return;}field.setAccessible(true);Map mapConfig = getFieldValue(field, configuration);if (Objects.nonNull(mapConfig)) {mapConfig.clear();}}private void clearSet(Class<?> classConfig, Configuration configuration, String fieldName) {Field field = getDeclaredField(classConfig, fieldName);if (Objects.isNull(field)) {return;}field.setAccessible(true);Set setConfig = getFieldValue(field, configuration);if (Objects.nonNull(setConfig)) {setConfig.clear();}}private <T> T getFieldValue(Field field, Object obj) {T value = null;try {value = (T) field.get(obj);} catch (IllegalAccessException e) {}return value;}private Field getDeclaredField(Class aClass, String fieldName) {Field field = null;try {field = aClass.getDeclaredField(fieldName);} catch (NoSuchFieldException e) {}return field;}
}

使用

在application.properties配置文件里添加如下配置,就会开启Mybatis Mapper的热更新。如果不配置或者配置值为false,则不会开启热更新。

由于此工具的原理是定时10秒一次比较文件是否变化,而判断文件变化的标准是编译路径target目录下的xml文件长度和最新一次修改时间是否发生变化,所以如果只是在idea里修改xml文件内容是不会触发Mybatis Mapper重载的,需要对resources包下的xml文件进行Recompile,这样target下的xml文件才会产生变化,从而触发Mybatis Mapper的重载。

mybatis.mapper.reload=true

原理

首先我们要知道Mybatis动态SQL的实现原理是什么?Mybatis是通过XML里的配置,利用JDK动态代理技术对Mapper接口增强,实现了写接口+写SQL就能直接操作数据库的功能,其他的JDBC所需要的加载驱动、建立连接、获取实体等都在Mybatis的增强逻辑里统一处理了,业务开发人员可以完全复用。

知道了这一点以后,我们需要搞清楚Mybatis是在什么时候读取的XML配置?读取的配置放在了哪里?要想实现Mybatis的SQL热更新,我们只要重新加载一次XML配置是不是就行了?

如何重载配置

Mybatis所有的配置都会加载到Configuration这个类里,在项目启动时 Mybatis 的 SqlSessionFactoryBuilder 就会读取 Mybatis XML 的配置。

其中的 MappedStatements 就是用来保存 Mapper XML 中的 SQL 语句的。

项目启动时,除了会加载 Mybatis XML 配置文件 mappers 标签的配置,还会加载 properties、settings、plugins 和 environments 等标签的配置,当前我们只需要关心 mappers 标签是如何加载的。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><properties resource="dbconfig.properties"/><settings><setting name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl"/></settings><plugins><plugin interceptor="com.github.pagehelper.PageInterceptor"><property name="helperDialect" value="org.apache.ibatis.page.MyMySqlDialect"/></plugin></plugins><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="${jdbc.driver}"/><property name="url" value="${jdbc.url}"/><property name="username" value="${jdbc.username}"/><property name="password" value="${jdbc.password}"/></dataSource></environment></environments><mappers><!--resource--><mapper resource="UserMapper.xml"/><!--class--><!-- <mapper class="org.apache.ibatis.mapper.UserMapper"/> --><!--url--><!-- <mapper url="D:\coder_soft\idea_workspace\ecard_bus\spring-boot-analyze\target\classes\UserMapper.xml"/> --><!--package--><!-- <package name="org.apache.ibatis.mapper" />--></mappers>
</configuration>

从 Mybatis XML 配置文件中我们可以看到 mappers 配置支持四种类型:

  • resource。从资源包下的 XML 配置加载。
  • class。从 Mapper 的 Class 接口的全限定名加载。
  • url。从 XML 配置的绝对路径加载。
  • package。从包的全限定名加载。

承接上图中的源码,mapperElement() 方法其实就是做了这一件事情,即根据配置中的 mappers 加载类型来加载 Mapper XML 配置。本文的 SQL 热更新类采用的是其中的resource方式。

总结一下,Mybatis 会将我们写的业务 SQL 通过 XML 配置里指定的路径加载到 Configuration 的 mappedStatements 这个 Map 类型的变量里。所以我们在重载 Mybatis 配置的时候,只需要更新 mappedStatements 相关的数据即可。如何重载配置呢?使用和 Mybatis 源代码加载时一样的方法即可。

 // 获取文件的输入流
InputStream inputStream = Resources.getResourceAsStream(resource);
// 使用XMLMapperBuilder解析Mapper文件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();

建议点赞+收藏+关注,方便以后复习查阅。

重载哪些配置

要想弄清楚需要重载哪些配置,我们可以看看 Mybatis 源码里看看加载 mappers 都做了什么事情?

首先判断 resources 有没有被解析过,如果已经被解析过则不再重新解析。我们现在要重载,所以肯定是要重新解析 Mapper XML 这些资源文件的,所以 Configuration 的 loadedResources 需要被重载

看下图,接下来进入 if 判断里,重点关注第114行代码,即 Mybatis 如何处理 Mapper 节点。第116行是将解析后的资源加到 Configuration 的 loadedResources 里。第118行是将 mapper 注册到 Configuration 里。

Mybatis 处理 Mapper 节点,实际上就是处理 Mapper XML 的各种标签。

Mapper XML 里的标签如下图,这是我上 Mybatis 官网截取的,结合 Mybatis 的源代码一目了然。

每个标签的处理流程都大相径庭,最终都会以 Configuration 的某个属性作为处理结果保存起来,下面我仅以 Cache 举例介绍一下。进入 cacheElement 方法,利用 XNode 读取 Cache 标签的各种属性,并作为参数调用 builderAssistant#useNewCache() 方法。

找到 builderAssistant#useNewCache() 方法最下面的一行代码,发现在构建了 Cache 对象之后,将改缓存对象加入到了 Configuration 里。

所以我们最终要重载的配置如下图,都在 Configuration 里了,它们除了 loadedResources 是 Set 集合以外,其他都是 Map 类型。

MapperHotSwap 解析

再一次贴上文章开头的流程图,对照着给大家讲解 MapperHotSwap 的实现原理。

热更新初始化

  • 56行:判断热更新是否开启;
  • 57行:读取 Mybatis Mapper 配置;
  • 58~65行:开启异步线程定时执行 SQL 热更新。

读取 Mybatis Mapper 配置

  • 76行:从 SqlSessionFactory 里获取 Configuration 配置;
  • 77行:从 MybatisPlus 配置项里获取 mapperLocations,MybatisPlus 默认配置的路径是 "classpath*:/mapper/**/*.xml"。也可以从 Mybatis 的配置项里读取,但需要手动在 application.properties 配置文件中添加 mybatis.mapper-locations 的配置。
  • 78~82行:遍历 mapperLocations ,将 Mapper XML 资源配置的初始帧值保存到 fileChange 这个 Map 对象里。帧值是由文件长度和文件最后一次修改时间之和组成的。

开启异步线程定时执行 SQL 热更新

  • 90行:判断 Mapper XML 是否变化;
  • 92行:清理上一次加载的 Mapper XML 配置;
  • 94~101行:遍历 mapperLocations ,调用 Mybatis 源码重载配置,这个在前文已经提到过了,不再赘述。

清除上一次加载的 Mapper XML 配置项。前文已经介绍过需要重载的配置项有哪些,这里需要清除的就是前文提到的几个配置,它们都是 Mapper XML 的标签在 Configuration 里的映射属性。除了 loadedResources 不是 Map 类型以外,因为只有 loadedResources 属性不是 XML 标签。我这里是从父类中遍历查找 loadedResources 属性,因为我用的是 MybatisPlus,MybatisPlus的配置类是 Configuration 的子类 MybatisConfiguration ,如果不从父类中查找会找不到,loadedResources 属性不会被清除,Mybatis 会认为 XML 已经被加载过,从而不会重载 XML 资源。

怎么样?对 Mybatis 这样介绍一番之后,是不是顿时觉得非常的简单了。“IT果果日记”会定期更新技术文章,欢迎大家多多关注。

建议点赞+收藏+关注,方便以后复习查阅。

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

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

相关文章

FineBI学习:K线图

效果图 底表结构&#xff1a;日期、股票代码、股票名称、开盘价、收盘价、最高价、最低价 步骤&#xff1a; 横轴&#xff1a;日期 纵轴&#xff1a;开盘价、最低价 选择【自定义图表】&#xff0c;或【瀑布图】 新建字段&#xff1a;价差&#xff08;收盘-开盘&#xf…

POETIZE个人博客系统源码 | 最美博客

源码介绍 POETIZE个人博客系统源码 | 最美博客 这是一个 SpringBoot Vue2 Vue3 的产物&#xff0c;支持移动端自适应&#xff0c;配有完备的前台和后台管理功能。 网站分两个模块&#xff1a; 博客系统&#xff1a;具有文章&#xff0c;表白墙&#xff0c;图片墙&#xf…

CSS 伪类、伪元素的应用实例:电池充电、高能进度条

一、目的 本文通过 CSS 伪类、伪元素&#xff0c;结合动画 animation 和 Vue 动态样式属性&#xff08;通过 CSS 变量&#xff09;的写法&#xff0c;来实现电池充电、高能进度条的效果&#xff0c;如下图所示。 二、基础知识 1、CSS 伪类、伪元素 简单概括成以下 4 点&#x…

谷粒商城实战(020 RabbitMQ-消息确认)

Java项目《谷粒商城》架构师级Java项目实战&#xff0c;对标阿里P6-P7&#xff0c;全网最强 总时长 104:45:00 共408P 此文章包含第258p-第p261的内容 消息确认 生产者 publishers 消费者 consumers 设置配置类 调用api 控制台 抵达brocker 代理 新版本ReturnCallbac…

DevEco Studio mac版启动不了【鸿蒙开发Bug已解决】

文章目录 项目场景:问题描述原因分析:解决方案:此Bug解决方案总结Bug解决方案寄语项目场景: 最近也是遇到了这个问题,看到网上也有人在询问这个问题,本文总结了自己和其他人的解决经验,解决了【DevEco Studio mac版启动不了】的问题。 问题描述 报错如下。 -------…

如何判断第三方软件测试公司是否具有资质

在软件开发的过程中&#xff0c;软件测试是确保软件质量、稳定性和用户体验的关键环节。许多企业选择将软件测试工作交给专业的第三方软件测试公司来完成&#xff0c;以确保测试的准确性和公正性。但是&#xff0c;如何判断一个第三方软件测试公司是否具有资质呢&#xff1f;以…

Python urllib 爬虫入门(2)

本文为Python urllib类库爬虫更入门的一些操作和爬虫实例及源码。 目录 模拟浏览器请求 简单模拟 设置随机user-agent 请求超时 HTTP请求类型 Get请求 Post请求 抓取网页动态请求 封装ajax请求 调用 循环调用 抓取小说 封装请求函数 把html写入本地分析 调用 正…

2024年Docker常用操作快速查询手册

目录 一、Linux系统上 Docker安装流程&#xff08;以ubuntu为例&#xff09; 一、卸载所有冲突的软件包 二、设置Docker的apt存储库&#xff08;这里使用的是阿里云软件源&#xff09; 三、直接安装最新版本的Docker 三、安装指定版本的Docker 四、验证Docker是否安装成功…

Linux 手动部署JDK21 环境

1、下载包&#xff08;我下载的是tar) https://www.oracle.com/cn/java/technologies/downloads/#java21 完成后进行上传 2、检查已有JDK&#xff0c;并删除&#xff08;我原有是jdk8&#xff09; rpm -qa | grep -i java | xargs -n1 rpm -e --nodeps3、清理掉 profile中的j…

Mybatis-Plus扩展接口InnerInterceptor

InnerInterceptor 接口就是 MyBatis-Plus 提供的一个拦截器接口&#xff0c;用于实现一些常用的 SQL 处理逻辑&#xff0c;处理 MyBatis-Plus 的特定功能,例如PaginationInnerInterceptor、OptimisticLockerInnerInterceptor 等,都实现了 InnerInterceptor 接口&#xff0c;并添…

Vue项目打包APK----Vue发布App

时隔多年我又来跟新了&#xff0c;今天给大普家及下前端Vue傻瓜式发布App&#xff0c;话不多说直接上干货。 首先准备开发工具HBuilder X&#xff0c;去官网直接下载即可&#xff0c;算了直接给你们上地址吧HBuilderX-高效极客技巧。 打开软件&#xff0c;文件-->新建--&g…

【XR806开发板试用】基于XR806实现智能小车

一、实验功能&#xff1a; 1、 基于XR806实现WIFI连接路由器 2、 XR806设备创建TCP socket服务器&#xff0c;局域网内通过PC端TCP客服端连接XR806 TCP服务器进行指令控制小车运行&#xff08;指令&#xff21;&#xff1a;前进、&#xff22;&#xff1a;后退、&#xff23;&…

实验15 MVC

二、实验项目内容&#xff08;实验题目&#xff09; 编写代码&#xff0c;掌握MVC的用法。 三、源代码以及执行结果截图&#xff1a; inputMenu.jsp&#xff1a; <% page contentType"text/html" %> <% page pageEncoding "utf-8" %> &…

Nginx配置Https缺少SSL模块

1、Linux下Nginx配置https nginx下载和安装此处就忽略&#xff0c;可自行百度 1.1、配置https 打开nginx配置文件 vim /opt/app/nginx/conf/nginx.conf相关https配置 server {listen 443 ssl; #开放端口server_name echarts.net;#域名#redirect to https#ssl on; #旧版#ssl证…

C语言-嵌入式-STM32:FreeRTOS说明和详解

Free即免费的&#xff0c;RTOS的全称是Real time operating system&#xff0c;中文就是实时操作系统。 注意&#xff1a;RTOS不是指某一个确定的系统&#xff0c;而是指一类操作系统。比如&#xff1a;uc/OS&#xff0c;FreeRTOS&#xff0c;RTX&#xff0c;RT-Thread 等这些都…

应用监控(Prometheus + Grafana)

可用于应用监控的系统有很多&#xff0c;有的需要埋点(切面)、有的需要配置Agent(字节码增强)。现在使用另外一个监控系统 —— Grafana。 Grafana 监控面板 这套监控主要用到了 SpringBoot Actuator Prometheus Grafana 三个模块组合的起来使用的监控。非常轻量好扩展使用。…

JDK-Mac系统和Windows系统安装及Java版本新特性(java9 - java19)

过去岁月不可追&#xff0c; 未来日子要珍惜。 莫愁身外七八事&#xff0c; 且尽眼前两三杯。 当你纠结于过去之时&#xff0c;懊恼与悔恨难免会让你陷入不欢。 当你忧愁于未来之时&#xff0c;未知与不安又会逐渐侵蚀你的心灵。 勿要纠结于过去&#xff0c;勿要忧愁于未来&…

使 Elasticsearch 和 Lucene 成为最佳向量数据库:速度提高 8 倍,效率提高 32 倍

作者&#xff1a;来自 Elastic Mayya Sharipova, Benjamin Trent, Jim Ferenczi Elasticsearch 和 Lucene 成绩单&#xff1a;值得注意的速度和效率投资 我们 Elastic 的使命是将 Apache Lucene 打造成最佳的向量数据库&#xff0c;并继续提升 Elasticsearch 作为搜索和 RAG&a…

透视天气:数据可视化的新视角

数据可视化在天气方面能够为我们带来极大的帮助。天气是人类生活中一个重要的因素&#xff0c;对于农业、交通、航空、能源等各个领域都有着重要的影响。而数据可视化技术通过将复杂的天气数据转化为直观、易懂的图表、图像或地图等形式&#xff0c;为我们提供了更深入、更全面…

如何提升制造设备文件汇集的可靠性和安全性?

制造设备文件汇集通常指的是将与制造设备相关的各种文档和资料进行整理和归档的过程。这些文件可能包括但不限于&#xff1a; 生产数据&#xff1a;包括生产计划、订单信息、生产进度等。 设计文件&#xff1a;如CAD图纸、设计蓝图、产品模型等。 工艺参数&#xff1a;用于指…