一个类实现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;

    @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.mfbz.cn/a/585729.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

前端打包过大如何解决?

前端开发完毕部署到线上是&#xff0c;执行npm run build。当打包过大时&#xff0c;部署到服务端后加载缓慢&#xff0c;如何优化&#xff1f; 我们可以通过执行npm run analyze。可以看到各个包文件大小的区别。 当打包过大时&#xff0c;通过压缩gzip的方式&#xff0c;可以…

React路由导航

1. 什么是路由导航 一个路由跳转到另一个路由&#xff0c;并且在跳转的同时有可能需要传递参数进行通信&#xff0c;比如列表页跳转到详情页携带参数 2. 声明式导航 声明式导航是指通过在模版中通过 <Link/> 组件描述出要跳转到哪里去&#xff0c;比如后台管理系统的…

【LeetCode】---15.最小栈

【LeetCode】---15.最小栈 一、题目解析&#xff1a;二、算法原理&#xff1a;三、代码实现&#xff1a; 一、题目解析&#xff1a; 设计一个支持 push &#xff0c;pop &#xff0c;top 操作&#xff0c;并能在常数时间内检索到最小元素的栈。 实现 MinStack 类: MinStack() 初…

ARP学习及断网攻击

1.什么是ARP ARP&#xff08;Address Resolution Protocol&#xff09;是一种用于在IPv4网络中将IP地址映射到MAC地址的协议。在计算机网络中&#xff0c;每个网络接口都有一个唯一的MAC地址&#xff08;Media Access Control address&#xff09;&#xff0c;用于识别网络设备…

形态学图像处理

首先自己随便写了一个单词&#xff0c;然后在周围画一些相对细一点的噪声。 # 读取原始图片 original cv2.imread("romance.jpg") # 构造一个全1的5*5矩阵 kernel np.ones((5, 5), np.int8) 腐蚀 腐蚀&#xff08;Erosion&#xff09;是形态学图像处理中的一种基本…

Linux操作系统·进程管理

一、什么是进程 1.作业和进程的概念 Linux是一个多用户多任务的操作系统。多用户是指多个用户可以在同一时间使用计算机系统&#xff1b;多任务是指Linux可以同时执行几个任务&#xff0c;它可以在还未执行完一个任务时又执行另一项任务。为了完成这些任务&#xff0c;系统上…

初识Linux -- Linux的背景和发展史介绍

点赞关注不迷路&#xff01;&#xff0c;本节涉及初识Linux&#xff0c;主要为背景介绍和xshell登录主机。 1.Linux背景 1.1 发展史 Linux从哪里来&#xff1f;它是怎么发展的&#xff1f;在这里简要介绍Linux的发展史。 要说Linux&#xff0c;还得从UNIX说起。 1.2 UNIX发…

水下机器人(ROV)中继器(TMS)究竟是个啥?

前段时间公众号后台有人问释放ROV的装置&#xff0c;由于只用过观察级ROV Valor&#xff0c;博主一直以为他说的是绞车&#xff0c;后来才明白他说的是中继器&#xff0c;在水中用来释放、控制和回收ROV的装置。 中继器TMS的全称是缆绳管理系统Tether Management System&#…

Linux基础——Linux开发工具(下)_make/makefile

前言&#xff1a;在经过前面两篇学习&#xff0c;大家对Linux开发工具都有一定的了解&#xff0c;而在此之前最重要的两个工具就是vim&#xff0c;gcc。 如果对这两个工具不太了解&#xff0c;可以先阅读这两篇文章&#xff1a; Linux开发工具 (vim) Linux开发工具 (gcc/g) 首先…

vue 时间轴页面 自己的写法 欢迎交流指正

<div class"first-box"><!--贯穿线--><div class"vertical-line-wrap"><div class"vertical-line"></div><div class"vertical-line-arrow"></div></div><!--开始--><div c…

多输入多输出 | Matlab实现WOA-LSSVM鲸鱼算法优化最小二乘支持向量机多输入多输出预测

多输入多输出 | Matlab实现WOA-LSSVM鲸鱼算法优化最小二乘支持向量机多输入多输出预测 目录 多输入多输出 | Matlab实现WOA-LSSVM鲸鱼算法优化最小二乘支持向量机多输入多输出预测预测效果基本介绍程序设计往期精彩参考资料 预测效果 基本介绍 Matlab实现WOA-LSSVM鲸鱼算法优化…

我五一是这样计划的,第一天...

前言 这个时间点&#xff0c;大多数人一定已经“峡谷做好准备全军出击”或者在出行的路上了。这个时间我也在回老家路上聊一聊。 行程 老读者都知道我老家在内蒙的西北的边陲城市&#xff0c;往年票都是随便买、除了春运几乎坐不满&#xff0c;今年五一居然也需要抢票&#…

用Python实现播放gif文件

用Python实现播放gif文件 在Python中&#xff0c;你可以使用第三方库Pillow&#xff08;PIL&#xff09;来加载和展示 GIF 文件。并实现“暂停”和“继续”控制功能。 Pillow是Python社区中最受欢迎的图像处理库之一&#xff0c;可以轻松地完成各种图像处理任务&#xff0c;它…

《21天学通C++》(第十二章)运算符类型与运算符重载

1.为什么要重载运算符&#xff1f; 通过重载运算符&#xff0c;可以将复杂的操作封装成简单的运算符形式&#xff0c;简化代码&#xff0c;提高可读性下面举一个简单的例子 计算两个点的坐标之和。 1.不重载运算符 #include <iostream> using namespace std; class P…

OpenHarmony实战开发-使用SmartPerf-Host分析应用性能

简介 SmartPerf-Host是一款深入挖掘数据、细粒度展示数据的性能功耗调优工具&#xff0c;可采集CPU调度、频点、进程线程时间片、堆内存、帧率等数据&#xff0c;采集的数据通过泳道图清晰地呈现给开发者&#xff0c;同时通过GUI以可视化的方式进行分析。该工具当前为开发者提…

docker在linux上的安装与使用

我的操作系统centos7本地vm docker安装 1、卸载旧版本 如果系统中已经存在旧的Docker&#xff0c;则先卸载 yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-engine 2、配置一…

Android 音视频基础知识

本系列文章会介绍两个 Android NDK Demo&#xff0c;拉流端会实现一个基于 FFmpeg 的视频播放器 Demo&#xff0c;推流端会实现一个视频直播 Demo&#xff0c;当然在做 Demo 之前会介绍音视频的基础知识。以下是本系列文章的目录&#xff1a; Android 音视频基础知识 Android 音…

【langchain】快速封装替换自定义LLM(基于自定义API或本地模型)

1. 引言 你可能已经注意到&#xff0c;LLM时代下的许多项目&#xff08;特别是Github上的论文项目、工程项目&#xff09;都要求我们设置OpenAI的API Key&#xff0c;就像这样&#xff1a; os.environ["OPENAI_API_KEY"] "sk-"from langchain_openai im…

SDKMAN!

概述 官网&#xff0c;SDKMAN是一款管理多版本SDK的工具&#xff0c;可以实现在多个版本间的快速切换。 其他特性&#xff1a; 易用&#xff1a;安装SDK不再需要去Google想安装的某个软件的官网的下载页&#xff0c;或找其他下载页面&#xff0c;然后下载安装包、解压、设置…

.NET C# ORM 瀚高数据库

SqlSugar ORM SqlSugar 是一款 老牌 .NET开源ORM框架&#xff0c;由果糖大数据科技团队维护和更新 &#xff0c;开箱即用最易上手的ORM 优点 &#xff1a;【生态丰富】【高性能】【超简单】 【功能全面】 【多库兼容】【适合产品】 【SqlSugar视频教程】 支持 &#xff1a…
最新文章