需求:

项目在开发阶段或是修复bug阶段,会有修改mybatis的mapper. 的时候,修改一般情况都要重启才能生失效,如果是分布式项目重启有时会耗时很久,都是无尽的等待。如果频繁修改,那么时间都浪费到等待重启的过程。

目标:

实现mybatis的mapper. 文件修改后热部署,而且只热更新修改了的 ,可以提高重新解析过程的效率。

要求:

尽量满足开闭原则

实现:

import com.yirun. work.core.utils.PropertiesHolder;
import org.apache.commons.lang.StringUtils;
import org.apache.ibatis.builder. . MapperBuilder;
import org.apache.ibatis.builder. . MapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spring work.beans.BeansException;
import org.spring work.beans.factory.InitializingBean;
import org.spring work.context.ApplicationContext;
import org.spring work.context.ApplicationContextAware;
import org.spring work.core.io.Resource;

import java.lang.reflect.Field;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Collectors;

/**
*  mapper. 热部署,最小单位是一个 文件
*  @date                    :2018/12/20
*  @author                  :zc.ding@foxmail.com
*/
public class MapperHotDeployPlugin implements InitializingBean, ApplicationContextAware {
    private final static Logger logger = LoggerFactory.getLogger(MapperHotDeployPlugin.class);
    private final static String OPEN = \"1\";
    private volatile SqlSessionFactoryBean sqlSessionFactoryBean;
    private volatile Configuration configuration;
    
    @Override
    public void afterPropertiesSet() {
        String flag = PropertiesHolder.getProperty(\"mapper.hot.deploy\");
        logger.info(\"Mybatis热部署标识mapper.hot.deploy={}\", flag);
        // 判断是否开启了热部署
        if(StringUtils.isNotBlank(flag) && OPEN.equals(flag)){
            new WatchThread().start();
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) applicationContext.getBean(\" workSqlSessionFactory\");
        sqlSessionFactoryBean = applicationContext.getBean(SqlSessionFactoryBean.class);
        configuration = sqlSessionFactory.getConfiguration();
    }
    
    class WatchThread extends Thread{
        private final Logger logger = LoggerFactory.getLogger(WatchThread.class);
        @Override
        public void run() {
            startWatch();
        }

        /**
         *  启动监听
         *  @date                    :2018/12/19
         *  @author                  :zc.ding@foxmail.com
         */
        private void startWatch(){
            try{
                WatchService watcher = FileSystems.getDefault().newWatchService();
                getWatchPaths().forEach(p -> {
                    try {
                        Paths.get(p).register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
                    } catch (Exception e) {
                        logger.error(\"ERROR: 注册 监听事件\", e);
                        throw new RuntimeException(\"ERROR: 注册 监听事件\", e);
                    }
                });
                while (true) {
                    WatchKey watchKey = watcher.take();
                    Set<String> set = new HashSet<>();
                    for (WatchEvent<?> event: watchKey.pollEvents()) {
                        set.add(event.context().toString());
                    }
                    // 重新加载 
                    reload (set);
                    boolean valid = watchKey.reset();
                    if (!valid) {
                        break;
                    }
                }
            }catch(Exception e){
                System.out.println(\"Mybatis的 监控失败!\");
                logger.info(\"Mybatis的 监控失败!\", e);
            }
        }

        /**
         *  加载需要监控的文件父路径
         *  @return java.util.Set<java.lang.String>
         *  @date                    :2018/12/19
         *  @author                  :zc.ding@foxmail.com
         */
        private Set<String> getWatchPaths(){
            Set<String> set = new HashSet<>();
            Arrays.stream(getResource()).forEach(r -> {
                try{
                    logger.info(\"资源路径:{}\", r.toString());
                    set.add(r.getFile().getParentFile().getAbsolutePath());
                }catch(Exception e){
                    logger.info(\"获取资源路径失败\", e);
                    throw new RuntimeException(\"获取资源路径失败\");
                }
            });
            logger.info(\"需要监听的 资源: {}\", set);
            return set;
        }

        /**
         *  获取配置的mapperLocations
         *  @return org.spring work.core.io.Resource[]
         *  @date                    :2018/12/19
         *  @author                  :zc.ding@foxmail.com
         */
        private Resource[] getResource(){
            return (Resource[]) getFieldValue(sqlSessionFactoryBean, \"mapperLocations\");
        }

        /**
         *  删除 元素的节点缓存
         *  @param nameSpace  中命名空间
         *  @date                    :2018/12/19
         *  @author                  :zc.ding@foxmail.com
         */
        private void clearMap(String nameSpace) {
            logger.info(\"清理Mybatis的namespace={}在mappedStatements、caches、resultMaps、parameterMaps、keyGenerators、sqlFragments中的缓存\");
            Arrays.asList(\"mappedStatements\", \"caches\", \"resultMaps\", \"parameterMaps\", \"keyGenerators\", \"sqlFragments\").forEach(fieldName -> {
                  value = getFieldValue(configuration, fieldName);
                if (value instanceof Map) {
                    Map<?, ?> map = (Map)value;
                    List< > list = map.keySet().stream().filter(o -> o.toString().startsWith(nameSpace + \".\")).collect(Collectors.toList());
                    logger.info(\"需要清理的元素: {}\", list);
                    list.forEach(k -> map.remove(( )k));
                }
            });
        }

        /**
         *  清除文件记录缓存
         *  @param resource  文件路径
         *  @date                    :2018/12/19
         *  @author                  :zc.ding@foxmail.com
         */
        private void clearSet(String resource) {
            logger.info(\"清理mybatis的资源{}在容器中的缓存\", resource);
              value = getFieldValue(configuration, \"loadedResources\");
            if (value instanceof Set) {
                Set<?> set = (Set)value;
                set.remove(resource);
                set.remove(\"namespace:\" + resource);
            }
        }

        /**
         *  获取对象指定属性
         *  @param obj 对象信息
         *  @param fieldName 属性名称
         *  @return java.lang. 
         *  @date                    :2018/12/19
         *  @author                  :zc.ding@foxmail.com
         */
        private   getFieldValue(  obj, String fieldName){
            logger.info(\"从{}中加载{}属性\", obj, fieldName);
            try{
                Field field = obj.getClass().getDeclaredField(fieldName);
                boolean accessible = field.isAccessible();
                field.setAccessible(true);
                  value = field.get(obj);
                field.setAccessible(accessible);
                return value;
            }catch(Exception e){
                logger.info(\"ERROR: 加载对象中[{}]\", fieldName, e);
                throw new RuntimeException(\"ERROR: 加载对象中[\" + fieldName + \"]\", e);
            }
        }

        /**
         *  重新加载set中 
         *  @param set 修改的 资源
         *  @date                    :2018/12/19
         *  @author                  :zc.ding@foxmail.com
         */
        private void reload (Set<String> set){
            logger.info(\"需要重新加载的文件列表: {}\", set);
            List<Resource> list = Arrays.stream(getResource())
                    .filter(p -> set.contains(p.getFilename()))
                    .collect(Collectors.toList());
            logger.info(\"需要处理的资源路径:{}\", list);
            list.forEach(r ->{
                try{
                    clearMap(getNamespace(r));
                    clearSet(r.toString());
                     MapperBuilder  MapperBuilder = new  MapperBuilder(r.getInputStream(), configuration,
                            r.toString(), configuration.getSqlFragments());
                     MapperBuilder.parse();
                }catch(Exception e){
                    logger.info(\"ERROR: 重新加载[{}]失败\", r.toString(), e);
                    throw new RuntimeException(\"ERROR: 重新加载[\" + r.toString() + \"]失败\", e);
                }finally {
                    ErrorContext.instance().reset();
                }
            });
            logger.info(\"成功热部署文件列表: {}\", set);
        }

        /**
         *  获取 的namespace
         *  @param resource  资源
         *  @return java.lang.String
         *  @date                    :2018/12/19
         *  @author                  :zc.ding@foxmail.com
         */
        private String getNamespace(Resource resource){
            logger.info(\"从{}获取namespace\", resource.toString());
            try{
                XPathParser parser = new XPathParser(resource.getInputStream(), true, null, new  MapperEntityResolver());
                return parser.evalNode(\"/mapper\").getStringAttribute(\"namespace\");
            }catch(Exception e){
                logger.info(\"ERROR: 解析 中namespace失败\", e);
                throw new RuntimeException(\"ERROR: 解析 中namespace失败\", e);
            }
        }
    }
}

待优化:

支持单数据源,使用开发环境

 

good luck!

收藏 打印