一个类实现Mybatis的SQL热更新

news2024/12/26 21:39:46

引言

平时用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;

    @Autowired
    private MybatisPlusProperties mybatisPlusProperties;
    @Autowired
    private 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)
    @Data
    public static class MapperHotSwapProperties {
        public final static String PREFIX = "mybatis.mapper";
        private Boolean reload = false;
    }

    @Autowired
    private MapperHotSwapProperties hotSwapProperties;

    @PostConstruct
    public 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.coloradmin.cn/o/1636557.html

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

相关文章

【webrtc】MessageHandler 2: 基于线程的消息处理:以PeerConnectionClient为例

PeerConnectionClient 前一篇 nullaudiopoller 并么有场景线程,而是就是在当前线程直接执行的, PeerConnectionClient 作为一个独立的客户端,默认的是主线程。 PeerConnectionClient 同时维护客户端的信令状态,并且通过OnMessage实现MessageHandler 消息处理。 目前只处理一…

CCF-CSP真题题解:201403-1 相反数

201403-1 相反数 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int MAXN 510;int n, a[MAXN]; int cnt[MAXN];int main() {scanf("%d", &n);for (int i 0; i < n; i) { scanf("%d"…

【分治算法】【Python实现】最接近点对

文章目录 [toc]问题描述一维最接近点对算法Python实现 二维最接近点对算法分治算法时间复杂性Python实现 个人主页&#xff1a;丷从心 系列专栏&#xff1a;分治算法 学习指南&#xff1a;Python学习指南 问题描述 给定平面上 n n n个点&#xff0c;找其中的一对点&#xff…

Python 深度学习(二)

原文&#xff1a;zh.annas-archive.org/md5/98cfb0b9095f1cf64732abfaa40d7b3a 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第五章&#xff1a;图像识别 视觉可以说是人类最重要的感官之一。我们依赖视觉来识别食物&#xff0c;逃离危险&#xff0c;认出朋友和家人…

【C++题解】1044. 找出最经济型的包装箱型号

问题&#xff1a;1044. 找出最经济型的包装箱型号 类型&#xff1a;多分支结构 题目描述&#xff1a; 已知有 A&#xff0c;B&#xff0c;C&#xff0c;D&#xff0c;E 五种包装箱&#xff0c;为了不浪费材料&#xff0c;小于 10 公斤的用 A 型&#xff0c;大于等于 10 公斤小…

浅论汽车研发项目数字化管理之道

随着汽车行业竞争不断加剧&#xff0c;汽车厂商能否快速、高质地推出贴合市场需求的新车型已经成为车企竞争的重要手段&#xff0c;而汽车研发具备流程复杂、专业领域多、协作难度大、质量要求高等特点&#xff0c;企业如果缺少科学健全的项目管理体系&#xff0c;将会在汽车研…

应用监控(Prometheus + Grafana)

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

光伏管理系统:降本增效解决方案。

现在是光伏发展的重要节点&#xff0c;如何在众多同行中脱颖而出并且有效的达到降低成本、提高效率也是很多企业都在考虑的问题&#xff0c;鹧鸪云的团队研发出了光伏管理系统&#xff0c;通过更高效、更智能、更全面的管理方式来帮助企业实现降本增效的转型&#xff0c;小编带…

记录AE学习查漏补缺(持续补充中。。。)

记录AE学习查漏补缺 常用win下截图WinShifts导入AI/PS工程文件将图层上移一个位置或者下移一个位置展示/关闭图层标线/标度放大面板适应屏幕大小 CtrlAltF 关键帧熟记关键参数移动锚点位置加选一个关键参数快速回到上下一帧隐藏/显示图层关键帧拉长缩短关键帧按着鼠标左键不松手…

【面试经典 150 | 回溯】单词搜索

文章目录 写在前面Tag题目来源解题思路方法一&#xff1a;回溯 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对于本题涉及到的数据结构等内容进行回顾…

Golang | Leetcode Golang题解之第59题螺旋矩阵II

题目&#xff1a; 题解&#xff1a; func generateMatrix(n int) [][]int {matrix : make([][]int, n)for i : range matrix {matrix[i] make([]int, n)}num : 1left, right, top, bottom : 0, n-1, 0, n-1for left < right && top < bottom {for column : lef…

Flask表单详解

Flask表单详解 概述跨站请求伪造保护表单类把表单渲染成HTML在视图函数中处理表单重定向和用户会话Flash消息 概述 尽管 Flask 的请求对象提供的信息足够用于处理 Web 表单&#xff0c;但有些任务很单调&#xff0c;而且要重复操作。比如&#xff0c;生成表单的 HTML 代码和验…

【stomp 实战】spring websocket用户消息发送源码分析

这一节&#xff0c;我们学习用户消息是如何发送的。 消息的分类 spring websocket将消息分为两种&#xff0c;一种是给指定的用户发送&#xff08;用户消息&#xff09;&#xff0c;一种是广播消息&#xff0c;即给所有用户发送消息。那怎么区分这两种消息呢?那就是用前缀了…

maven聚合,继承等方式

需要install安装到本地仓库&#xff0c;或者私服&#xff0c;方可使用自己封装项目 编译&#xff0c;测试&#xff0c;打包&#xff0c;安装&#xff0c;发布 parent: <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://mav…

Python 机器学习 基础 之 学习 基础环境搭建

Python 机器学习 基础 之 学习 基础环境搭建 目录 Python 机器学习 基础 之 学习 基础环境搭建 一、简单介绍 二、什么是机器学习 三、python 环境的搭建 1、Python 安装包下载 2、这里以 下载 Python 3.10.9 为例 3、安装 Python 3.10.9 4、检验 python 是否安装成功&…

二分图--判定以及最大匹配

水了个圈钱杯省一&#xff0c;不过估计国赛也拿不了奖&#xff0c;但还是小小挣扎一下。 什么是二分图&#xff1a;G(V,E)是一个无向图&#xff0c;若顶点V可以分为两个互不相交的子集A,B&#xff0c;并图中的每一条边&#xff08;i,j)所关联的ij属于不同的顶点集&#xff0c;…

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

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

Nginx实现端口转发与负载均衡配置

前言&#xff1a;当我们的软件体系结构较为庞大的时候&#xff0c;访问量往往是巨大的&#xff0c;所以我们这里可以使用nginx的均衡负载 一、配置nginx实现端口转发 本地tomcat服务端口为8082 本地nginx端口为8080 目的&#xff1a;将nginx的8080转发到tomcat的8082端口上…

每日一题:插入区间

给你一个 无重叠的 &#xff0c;按照区间起始端点排序的区间列表 intervals&#xff0c;其中 intervals[i] [starti, endi] 表示第 i 个区间的开始和结束&#xff0c;并且 intervals 按照 starti 升序排列。同样给定一个区间 newInterval [start, end] 表示另一个区间的开始和…

【嵌入式笔试题】网络编程笔试题

非常经典的笔试题。 2.网络编程(29道) 2.1列举一下OSI协议的各种分层。说说你最熟悉的一层协议的功能。 ( 1 )七层划分为:应用层、表示层、会话层、传输层、网络层、数据链路层、物理 层。 ( 2 )五层划分为:应用层、传输层、网络层、数据链路层、物理层。 ( 3 )…