热加载 MyBatis 中修改过的 Mapper.xml

Posted 诗歌poetry

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了热加载 MyBatis 中修改过的 Mapper.xml相关的知识,希望对你有一定的参考价值。

拿来即用的热加载 MyBatis 中修改过的 Mapper.xml

项目中使用的是 MyBatis, 在开发过程中, 每次修改完 SQL 都需要重新启动一遍项目, 非常耗时, 影响开发效率. 所以非常有必要热加载 修改过的 Mapper.xml 文件.

原理并不难 :

  • 遍历所有的 xml 文件, 根据文件属性, 找到刚更新的文件
  • 删除缓存中修改过的 xml 文件的解析对象
  • 重新解析 xml 文件并保存

使用方式: 直接复制下面的类到 项目中, 启动项目即可看到如下输出, 说明启动成功

========= Enabled refresh mybatis mapper =========

修改完 SQL 以后, 更新一下资源


过几秒钟, 控制台即可看到更新的文件.

直接复制下面代码到 Spring 项目中, 即可生效 如果没有使用 Lombok, 直接删掉日志输出或者使用 System.out.println() 输出

package com.util.mybatis;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Mybatis的mapper文件中的sql语句被修改后, 只能重启服务器才能被加载, 非常耗时,所以就写了一个自动加载的类,
 * 配置后检查xml文件更改,如果发生变化,重新加载xml里面的内容.
 */
@Slf4j
@Component
public class MapperAutoRefresh implements ApplicationContextAware, SmartInitializingSingleton 
    /*容器上下文, 通过 Aware 填充*/
    private ApplicationContext applicationContext;
    /*是否启用 Mapper 刷新线程功能*/
    private static boolean enabled = true;
    /* Mapper.xml 实际资源路径集合 默认去 MyBatis 中已有的配置*/
    private Set<String> locationSet = new HashSet<>();
    /* MyBatis 中 xml 文件的路径列表, 用 File[] 包裹, 没法直接使用*/
    private Set<String> loadedResourcesSet;
    /*MyBatis配置对象*/
    private Configuration configuration;
    /*上一次刷新时间*/
    private Long beforeTime = 0L;
    /*延迟刷新秒数*/
    private static int delaySeconds = 10;
    /*休眠时间*/
    private static int sleepSeconds = 30;

    /**
     * 根据配置的 SqlSessionFactoryBean 的 mapperLocations 属性, 获取所有的 mapper.xml 的资源路径
     *
     * @see SqlSessionFactoryBean
     */
    @SuppressWarnings("unchecked")
    public void setLocation() 
        try 
            Field loadedResourcesField = Configuration.class.getDeclaredField("loadedResources");
            loadedResourcesField.setAccessible(true);
            this.loadedResourcesSet = ((HashSet<String>) loadedResourcesField.get(configuration));
            for (String locationPath : loadedResourcesSet) 
                if (locationPath.startsWith("file [")) 
                    String s = locationPath.substring("file [".length(), locationPath.lastIndexOf("]"));
                    locationSet.add(s);
                    log.info("Location:" + s);
                
            
         catch (NoSuchFieldException | IllegalAccessException e) 
            throw new RuntimeException(e);
        
    

    /**
     * 执行资源刷新任务
     */
    public void exeTask() 
        if (CollectionUtils.isEmpty(locationSet)) 
            return;
        
        beforeTime = System.currentTimeMillis();
        if (enabled) 
            new Thread(runnable).start();
        
    

    private Runnable runnable = () -> 
        try 
            // 暂定时间
            TimeUnit.SECONDS.sleep(delaySeconds);
            log.info("========= Enabled refresh mybatis mapper =========");
            // 开始执行刷新操作
            while (true) 
                for (String path : locationSet) 
                    this.refresh(path, beforeTime);
                
                TimeUnit.SECONDS.sleep(sleepSeconds);
            
         catch (InterruptedException e) 
            throw new RuntimeException(e);
        
    ;

    /**
     * 刷新资源的操作
     *
     * @param filePath   xml 文件路径
     * @param beforeTime 上次刷新事件
     */
    public void refresh(String filePath, long beforeTime) 
        // 本次刷新时间
        long refreshTime = System.currentTimeMillis();
        File file = new File(filePath);
        if (!checkFile(file, beforeTime)) 
            return;
        
        try 
            InputStream inputStream = new FileInputStream(file);
            // 清理原有资源,更新为自己的StrictMap方便增量重新加载
            String[] mapFieldNames = new String[]
                    "mappedStatements", "caches",
                    "resultMaps", "parameterMaps",
                    "keyGenerators", "sqlFragments"
            ;
            for (String fieldName : mapFieldNames) 
                Field field = Configuration.class.getDeclaredField(fieldName);
                field.setAccessible(true);
                Map map = ((Map) field.get(configuration));
                if (!(map instanceof StrictMap)) 
                    Map newMap = new StrictMap(StringUtils.capitalize(fieldName) + "collection");
                    for (Object key : map.keySet()) 
                        try 
                            newMap.put(key, map.get(key));
                         catch (IllegalArgumentException ex) 
                            newMap.put(key, ex.getMessage());
                        
                    
                    field.set(configuration, newMap);
                
            
            // 清理已加载的资源标识,方便让它重新加载。
            this.loadedResourcesSet.remove(filePath);
            //重新编译加载资源文件。
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(inputStream, configuration,
                    filePath, configuration.getSqlFragments());
            xmlMapperBuilder.parse();
         catch (FileNotFoundException e) 
            e.printStackTrace();
         catch (NoSuchFieldException | IllegalAccessException e) 
            throw new RuntimeException(e);
         finally 
            ErrorContext.instance().reset();
        
        if (log.isDebugEnabled()) 
            log.info("Refresh file: " + file.getAbsolutePath());
            log.info("Refresh filename: " + file.getName());
        
        this.beforeTime = refreshTime;
    

    /**
     * 判断文件是否需要刷新,需要刷新返回true,否则返回false
     *
     * @param file       xml 文件
     * @param beforeTime 上次更新事件
     * @return 是否需要重新加载
     */
    private boolean checkFile(File file, Long beforeTime) 
        return file.lastModified() > beforeTime;
    


    /**
     * 重写 org.apache.ibatis.session.Configuration.StrictMap 类
     * 来自 MyBatis3.4.0版本,修改 put 方法,允许反复 put更新。
     *
     * @see org.apache.ibatis.session.Configuration.StrictMap
     */
    public static class StrictMap<V> extends HashMap<String, V> 

        private static final long serialVersionUID = -4950446264854982944L;
        private final String name;

        public StrictMap(String name, int initialCapacity, float loadFactor) 
            super(initialCapacity, loadFactor);
            this.name = name;
        

        public StrictMap(String name, int initialCapacity) 
            super(initialCapacity);
            this.name = name;
        

        public StrictMap(String name) 
            super();
            this.name = name;
        

        public StrictMap(String name, Map<String, ? extends V> m) 
            super(m);
            this.name = name;
        

        @SuppressWarnings("unchecked")
        @Override
        public V put(String key, V value) 
            // 核心逻辑, 先删除后添加
            if (enabled) 
                remove(key);
            
            if (containsKey(key)) 
                throw new IllegalArgumentException(name + " already contains value for " + key);
            
            if (key.contains(".")) 
                final String shortKey = getShortName(key);
                if (super.get(shortKey) == null) 
                    super.put(shortKey, value);
                 else 
                    super.put(shortKey, (V) new Ambiguity(shortKey));
                
            
            return super.put(key, value);
        

        @Override
        public V get(Object key) 
            V value = super.get(key);
            if (value == null) 
                throw new IllegalArgumentException(name + " does not contain value for " + key);
            
            if (value instanceof Ambiguity) 
                throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
                        + " (try using the full name including the namespace, or rename one of the entries)");
            
            return value;
        

        private String getShortName(String key) 
            final String[] keyParts = key.split("\\\\.");
            return keyParts[keyParts.length - 1];
        

        protected static class Ambiguity 
            private String subject;

            public Ambiguity(String subject) 
                this.subject = subject;
            

            public String getSubject() 
                return subject;
            
        

    

    /**
     * 单例实例化完成后执行
     */
    @Override
    public void afterSingletonsInstantiated() 
        SqlSessionFactory sessionFactory = applicationContext.getBean(SqlSessionFactory.class);
        this.configuration = sessionFactory.getConfiguration();
        setLocation();
        exeTask();
    

    /**
     * 赋值 applicationContext
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException 
        this.applicationContext = applicationContext;
    




以上是关于热加载 MyBatis 中修改过的 Mapper.xml的主要内容,如果未能解决你的问题,请参考以下文章

springboot 整合通用mapper , 热加载 遇到ClassCastException

牛叉了-arthas 热更新 mybatis mapper xml

牛叉了-arthas 热更新 mybatis mapper xml

牛叉了-arthas 热更新 mybatis mapper xml

牛叉了-arthas 热更新 mybatis mapper xml

解决JRebel对myBatis Mapper 失效的问题