一个类实现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.hqwc.cn/news/660473.html

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

相关文章

大模型公开课-大模型的语言解码游戏学习总结

在当今快速发展的人工智能领域&#xff0c;深度学习作为其中的一项关键技术&#xff0c;正引领着科技的新潮流。而对于初学者来说&#xff0c;了解大型语言模型的解码游戏&#xff0c;对于理解深度学习的基本概念至关重要。本篇博客将对一次关于大型语言模型解码游戏的视频教学…

Springboot+Vue项目-基于Java+MySQL的教学资料管理系统(附源码+演示视频+LW)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &…

Crocoddyl 使用教程(二)

系列文章目录 前言 小车摆杆是另一个经典的控制实例。在这个系统中&#xff0c;一根欠驱动的杆子被固定在一辆一维驱动的小车顶部。游戏的目的是将杆子升到站立位置。 模型如下&#xff1a; https://en.wikipedia.org/wiki/Inverted_pendulum 我们用 表示小车质量、 表示摆杆质…

Ubuntu 16.04下Firefox版本更新

最近要使用Odoo进行项目管理&#xff0c;Odoo17以上版本对浏览器版本要求较高&#xff0c;如果没有新版本下的函数&#xff0c;将无法运行。而Ubuntu16.04下自带的firefox不满足版本要求&#xff0c;因而需要手动下载安装。 查看当前系统版本apt-get能下载的firefox版本 apt-c…

使用 scikit-learn 进行机器学习的基本原理-2

介绍 scikit-learn 估计器对象 每个算法都通过“Estimator”对象在 scikit-learn 中公开。 例如&#xff0c;线性回归是&#xff1a;sklearn.linear_model.LinearRegression 估计器参数&#xff1a;估计器的所有参数都可以在实例化时设置&#xff1a; 拟合数据 让我们用 nump…

C++之const和指针

const在*号的左边时&#xff0c;此时const修饰的是指针指向的值。 例&#xff1a;const int * ptr&#xff0c;表明解引用之后的值&#xff0c;不可被改变&#xff0c;然而指针ptr呢&#xff0c;它可以指向其他的地址。 const在*号的右边时&#xff0c;此时const修饰的是指针…

监视器和显示器的区别,普通硬盘和监控硬盘的区别

监视器与显示器的区别&#xff0c;你真的知道吗&#xff1f; 中小型视频监控系统中&#xff0c;显示系统是最能展现效果的一个重要环节&#xff0c;显示系统的优劣将直接影响视频监控系统的用户体验满意度。 中小型视频监控系统中&#xff0c;显示系统是最能展现效果的一个重要…

二叉树:数据结构的分形之美

1.树形结构 1.1概念 树是一种非线性的数据结构&#xff0c;它是由n(n>0)个有限结点组成一个具有层次关系的集合。把他叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就说它的根朝上&#xff0c;而叶朝下的。它具有以下的特点&#xff1a; 有一个特殊的节点&#xff0…

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

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

ES 深度分页问题及针对不同需求下的解决方案[ES系列] - 第509篇

历史文章&#xff08;文章累计500&#xff09; 《国内最全的Spring Boot系列之一》 《国内最全的Spring Boot系列之二》 《国内最全的Spring Boot系列之三》 《国内最全的Spring Boot系列之四》 《国内最全的Spring Boot系列之五》 《国内最全的Spring Boot系列之六》 《…

AnyMP4 Blu-ray Ripper for Mac:您的蓝光影音转换专家

AnyMP4 Blu-ray Ripper for Mac&#xff0c;一款功能强大的蓝光影音转换软件&#xff0c;让您的蓝光内容焕发新生。 AnyMP4 Blu-ray Ripper for Macv9.0.58激活版下载 它采用最高效的解决方案&#xff0c;将蓝光光盘翻录为任何您想要的视频格式&#xff0c;无论是MP4、MKV还是A…

神经网络与深度学习(四)--自然语言处理NLP

这里写目录标题 1.序列模型2.数据预处理2.1特征编码2.2文本处理 3.文本预处理与词嵌入3.1文本预处理3.2文本嵌入 3.RNN模型3.1RNN概要3.2RNN误差反传 4.门控循环单元&#xff08;GRU&#xff09;4.1GRU基本结构 5.长短期记忆网络 (LSTM) 1.序列模型 分类问题与预测问题 图像分…