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

avatar
作者
筋斗云
阅读量:2

目录

一、背景

二、注意

三、代码

四、使用示例

五、其他参考博客


一、背景

开发一个报表功能,好几百行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博客

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!