修改了mybatis的xml中的sql不重启服务器如何动态加载更新

目录

一、背景

二、注意

三、代码

四、使用示例

五、其他参考博客


一、背景

开发一个报表功能,好几百行sql,每次修改完想自测下都要重启服务器,启动一次服务器就要3分钟,重启10次就要半小时,耗不起时间呀。于是在网上找半天,没发现能直接用的, 最后还是乖乖用了自己的业余时间,参考了网上内容写了个合适自己的类。

二、注意

1.本类在mybatis-plus-boot-starter 3.4.0, mybatis3.5.5下有效,其他版本没试过

2.部分idea版本修改了xml中的sql后,并不会直接写入到硬盘中,而是保留在内存中,需要手动ctrl+s或者切换到其他窗口才能触发写入新内容到硬盘,所以使用本类时要确认你修改的sql确实已经保存进硬盘里了

3.xml所在文件夹的绝对位置,需要你修改下,再使用本类
 

三、代码

用一个类就能实现开发阶段sql热更新

这个类可以配置启动一个线程,每10秒重新加载最近有修改过的sql

也可以调用一下接口重新修改过的sql

代码如下:

package com.gree;


import com.baomidou.mybatisplus.core.MybatisMapperRegistry;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;

/**
 * 本类用于热部署mybatis xml中修改的sql
 * 注意:
 *  1.本类在mybatis-plus-boot-starter 3.4.0 和 mybatis3.5.5 下有效,其他版本没试过
 *  2.部分idea版本修改了xml中的sql后,并不会直接写入到硬盘中,而是保留在内存中,需要手动ctrl+s或者切换到其他窗口才能触发写入新内容到硬盘,所以使用本类时要确认你修改的sql确实已经保存进硬盘里了
 *  3.xml所在文件夹的绝对位置,需要你修改下,再使用本类
 */
@RestController
public class MybatisMapperRefresh {


    //xml所在文件夹的绝对位置(这里改成你的位置)
    private String mapperPath = "D:\\wjh\\Mome\\openGitCode\\mybatisRefreshDemo\\src\\main\\resources\\mapper";
    //是否需要启动一个线程,每隔一段时间就刷新下
    private boolean needStartThread = false;
    //刷新间隔时间(秒)
    private int sleepSeconds = 10;




    //项目启动时间(或加载本class的时间)
    private long startTime = new Date().getTime();
    //上次执行更新xml的时间
    private long lasteUpdateTime = 0;

    private static final Log logger = LogFactory.getLog(MybatisMapperRefresh.class);

    private SqlSessionFactory sqlSessionFactory;
    private Configuration configuration;


    /**
     * 构造函数,由spring调用生成bean
     * @param sqlSessionFactory
     */
    public MybatisMapperRefresh(
            SqlSessionFactory sqlSessionFactory
    ) {
        this.sqlSessionFactory = sqlSessionFactory;
        this.configuration = sqlSessionFactory.getConfiguration();
        if (needStartThread) {
            this.startThread();
        }
    }


    /**
     * 调用这个接口刷新你的sql,接口会返回刷新了哪些xml
     * @return
     */
    @RequestMapping("/sql/refresh")
    public List<String> refreshMapper() {
        List<String> refreshedList = new ArrayList<>();
        try {
            refreshedList = refreshDir();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return refreshedList;
    }


    /**
     * 启动一个线程,每间隔一段时间就更新下xml中的sql
     */
    public void startThread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    logger.warn("线程循环中!");
                    try {
                        refreshDir();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    try {
                        Thread.sleep(sleepSeconds * 1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "mybatis-plus MapperRefresh").start();
    }


    /**
     * 刷新指定目录下所有xml文件
     *
     * @throws Exception
     */
    private List<String> refreshDir() throws Exception {
        List<String> refreshedList = new ArrayList<>();
        try {
            //获取指定目录下,修改时间大于上次刷新时间,并且修改时间大于项目启动时间的xml
            List<File> fileList = FileUtil.getAllFiles(mapperPath);
            ArrayList<File> needUpdateFiles = new ArrayList<>();
            for (File file : fileList) {
                long lastModified = file.lastModified();
                if (file.isFile() && startTime <= lastModified && lasteUpdateTime <= lastModified) {
                    needUpdateFiles.add(file);
                    continue;
                }
            }
            //逐个xml刷新
            if (needUpdateFiles.size() != 0) {
                lasteUpdateTime = new Date().getTime();
            }
            for (File file : needUpdateFiles) {
                Resource refresh = refresh(new FileSystemResource(file));
                if(refresh != null){
                    refreshedList.add(refresh.getFile().getAbsolutePath());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //返回已刷新的文件
        return refreshedList;
    }


    /**
     * 刷新mapper
     */
    private Resource refresh(Resource resource) throws Exception {


        //打印一下流,看看有没有获取到更新的内容
        /*InputStream inputStream = resource.getInputStream();
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            System.out.println(line);
        }
        bufferedReader.close();*/


        boolean isSupper = configuration.getClass().getSuperclass() == Configuration.class;
        try {
            //清理loadedResources
            //loadedResources:用于注册所有 Mapper XML 配置文件路径
            Field loadedResourcesField = isSupper
                    ? configuration.getClass().getSuperclass().getDeclaredField("loadedResources")
                    : configuration.getClass().getDeclaredField("loadedResources");
            loadedResourcesField.setAccessible(true);
            Set<String> loadedResourcesSet = ((Set<String>) loadedResourcesField.get(configuration));
            loadedResourcesSet.remove(resource.toString());

            //分析需要刷新的xml文件
            XPathParser xPathParser = new XPathParser(resource.getInputStream(), true, configuration.getVariables(),
                    new XMLMapperEntityResolver());
            //得到xml中的mapper节点
            XNode xNode = xPathParser.evalNode("/mapper");
            String xNodeNamespace = xNode.getStringAttribute("namespace");

            //清理mapperRegistry中的knownMappers
            //mapperRegistry:用于注册 Mapper 接口信息,建立 Mapper 接口的 Class 对象和 MapperProxyFactory 对象之间的关系,其中 MapperProxyFactory 对象用于创建 Mapper 动态代理对象
            Field knownMappersField = MybatisMapperRegistry.class.getDeclaredField("knownMappers");
            knownMappersField.setAccessible(true);
            Map knownMappers = (Map) knownMappersField.get(configuration.getMapperRegistry());
            knownMappers.remove(Resources.classForName(xNodeNamespace));

            //清理caches
            //caches:用于注册 Mapper 中配置的所有缓存信息,其中 Key 为 Cache 的 id,也就是 Mapper 的命名空间,Value 为 Cache 对象
            configuration.getCacheNames().remove(xNodeNamespace);

            //其他清理操作
            cleanParameterMap(xNode.evalNodes("/mapper/parameterMap"), xNodeNamespace);
            cleanResultMap(xNode.evalNodes("/mapper/resultMap"), xNodeNamespace);
            cleanKeyGenerators(xNode.evalNodes("insert|update|select|delete"), xNodeNamespace);
            cleanMappedStatements(xNode.evalNodes("insert|update|select|delete"), xNodeNamespace);
            cleanSqlElement(xNode.evalNodes("/mapper/sql"), xNodeNamespace);

            //重新加载xml文件
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(resource.getInputStream(),
                    configuration, resource.toString(),
                    configuration.getSqlFragments());
            xmlMapperBuilder.parse();


            logger.warn("重新加载成功: " + resource );
            return resource;
        } catch (IOException e) {
            logger.error("重新加载失败 :" ,e);
        } finally {
            ErrorContext.instance().reset();
        }
        return null;
    }

    /**
     * 清理parameterMap
     * parameterMap用于注册 Mapper 中通过 标签注册的参数映射信息。Key 为 ParameterMap 的 id,由 Mapper 命名空间和 标签的 id 属性构成,Value 为解析 标签后得到的 ParameterMap 对象
     *
     * @param list
     * @param namespace
     */
    private void cleanParameterMap(List<XNode> list, String namespace) {
        for (XNode parameterMapNode : list) {
            String id = parameterMapNode.getStringAttribute("id");
            configuration.getParameterMaps().remove(namespace + "." + id);
        }
    }

    /**
     * 清理resultMap
     * resultMap用于注册 Mapper 配置文件中通过 标签配置的 ResultMap 信息,ResultMap 用于建立 Java 实体属性与数据库字段之间的映射关系,其中 Key 为 ResultMap 的 id,该 id 是由 Mapper 命名空间和 标签的 id 属性组成的,Value 为解析 标签后得到的 ResultMap 对象
     *
     * @param list
     * @param namespace
     */
    private void cleanResultMap(List<XNode> list, String namespace) {
        for (XNode resultMapNode : list) {
            String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
            configuration.getResultMapNames().remove(id);
            configuration.getResultMapNames().remove(namespace + "." + id);
            clearResultMap(resultMapNode, namespace);
        }
    }


    /**
     * 清理ResultMap
     * ResultMap用于注册 Mapper 配置文件中通过 标签配置的 ResultMap 信息,ResultMap 用于建立 Java 实体属性与数据库字段之间的映射关系,其中 Key 为 ResultMap 的 id,该 id 是由 Mapper 命名空间和 标签的 id 属性组成的,Value 为解析 标签后得到的 ResultMap 对象
     */
    private void clearResultMap(XNode xNode, String namespace) {
        for (XNode resultChild : xNode.getChildren()) {
            if ("association".equals(resultChild.getName()) || "collection".equals(resultChild.getName())
                    || "case".equals(resultChild.getName())) {
                if (resultChild.getStringAttribute("select") == null) {
                    configuration.getResultMapNames()
                            .remove(resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
                    configuration.getResultMapNames().remove(namespace + "."
                            + resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
                    if (resultChild.getChildren() != null && !resultChild.getChildren().isEmpty()) {
                        clearResultMap(resultChild, namespace);
                    }
                }
            }
        }
    }

    /**
     * 清理keyGenerators
     * keyGenerators:用于注册 KeyGenerator,KeyGenerator 是 MyBatis 的主键生成器,MyBatis 提供了三种KeyGenerator,即 Jdbc3KeyGenerator(数据库自增主键)、NoKeyGenerator(无自增主键)、SelectKeyGenerator(通过 select 语句查询自增主键,例如 oracle 的 sequence)
     *
     * @param list
     * @param namespace
     */
    private void cleanKeyGenerators(List<XNode> list, String namespace) {
        for (XNode xNode : list) {
            String id = xNode.getStringAttribute("id");
            configuration.getKeyGeneratorNames().remove(id + SelectKeyGenerator.SELECT_KEY_SUFFIX);
            configuration.getKeyGeneratorNames().remove(namespace + "." + id + SelectKeyGenerator.SELECT_KEY_SUFFIX);
        }
    }


    /**
     * 清理MappedStatements
     * MappedStatement 对象描述 <insert|selectlupdateldelete> 等标签或者通过 @Select|@Delete|@Update|@Insert 等注解配置的 SQL 信息。MyBatis 将所有的 MappedStatement 对象注册到该属性中,其中 Key 为 Mapper 的 Id, Value 为 MappedStatement 对象
     *
     * @param list
     * @param namespace
     */
    private void cleanMappedStatements(List<XNode> list, String namespace) {
        Collection<MappedStatement> mappedStatements = configuration.getMappedStatements();
        List<MappedStatement> objects = new ArrayList<>();
        for (XNode xNode : list) {
            String id = xNode.getStringAttribute("id");
            Iterator<MappedStatement> it = mappedStatements.iterator();
            while (it.hasNext()) {
                Object object = it.next();
                if (object instanceof org.apache.ibatis.mapping.MappedStatement) {
                    MappedStatement mappedStatement = (MappedStatement) object;
                    if (mappedStatement.getId().equals(namespace + "." + id)) {
                        objects.add(mappedStatement);
                    }
                }
            }
        }
        mappedStatements.removeAll(objects);
    }


    /**
     * 清理sql节点缓存
     * 用于注册 Mapper 中通过 标签配置的 SQL 片段,Key 为 SQL 片段的 id,Value 为 MyBatis 封装的表示 XML 节点的 XNode 对象
     *
     * @param list
     * @param namespace
     */
    private void cleanSqlElement(List<XNode> list, String namespace) {
        for (XNode context : list) {
            String id = context.getStringAttribute("id");
            configuration.getSqlFragments().remove(id);
            configuration.getSqlFragments().remove(namespace + "." + id);
        }
    }


    public static class FileUtil {
        /**
         * 列出执行文件夹下的所有文件,包含子目录文件
         */
        public static List<File> getAllFiles(String folderPath) {
            List<File> fileList = new ArrayList<>();
            File folder = new File(folderPath);
            if (!folder.exists() || !folder.isDirectory()) {
                return fileList;
            }
            File[] files = folder.listFiles();
            for (File file : files) {
                if (file.isFile()) {
                    fileList.add(file);
                } else if (file.isDirectory()) {
                    fileList.addAll(getAllFiles(file.getAbsolutePath()));
                }
            }
            return fileList;
        }
    }
}

pom.xml文件也贴出来给大家参考

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.greetree</groupId>
    <artifactId>mybatisRefreshDemo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>


    <properties>
        <java.version>1.8</java.version>
    </properties>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>

        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>


    </dependencies>


</project>

四、使用示例

1. 将类MybatisMapperRefresh粘贴到可以被spring扫描到的任意目录

2. 修改类中的mapperPath,改成你的xml文件所在目录

3. 启动服务器

4. 修改你的xml文件里的sql

5.ctrl+s保存文件,确保idea将修改内容写入到硬盘,而不是在内存中

6.调用接口http://localhost:{你项目端口号}/sql/refresh 更新sql,接口会返回刷新了哪些xml

7.验证你的sql是否热更新了

五、其他参考博客

IDEA的热部署【MyBatis XML热部署 】_怎么配热部署实现更新ibatis的xml文件-CSDN博客

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值