JavaWeb-Mybatis动态刷新XML

简介: 利用多线程实现Mybatis动态刷新XML,做到不重启服务完成XML中的SQL注入

mybatis-1

使用Mybatis过程中,很多时候修改了XML文件需要整个项目重新启动,比较耗时,如果没什么业务数据状态还好,有数据状态可就惨啦,所以XML自动线下更新就很有必要。手写一个简单实现,大家参考下。

我的实现思路就是利用一个额外线程扫描mybatis XML文件,更新到 Spring中的 上下文ApplicationContext中。

1. 配置文件

我们定义一套刷新时间和周期频次的配置文件在路径 persistence-mybatis\mybatis-base\src\main\resources\conf\mybatis-refresh.properties 中,里面内容如下:

enabled=true
delaySeconds=30
sleepSeconds=10
mappingPath=mapper
  • enabled:是否开启自动刷新
  • delaySeconds: 间隔时间
  • sleepSeconds: 休眠时间
  • mappingPath:XML的路径

核心类需要实现上下文接口 ApplicationContextAware

2. 关键步骤

  • @Override重写setApplicationContext 方法
  • 用静态语句块,初始化配置文件中的相关参数
  • @PostConstruct:在构造函数之后对SqlSessionFactory进行额外配置
  • 启用线程按照频次间隔重复执行上述操作

关键性步骤如下:

// 1、从上下文容器获取 SqlSessionFactory
SqlSessionFactory sessionFactory = applicationContext.getBean(SqlSessionFactory.class);
// 2、获取Configuration
Configuration configuration = sessionFactory.getConfiguration();
this.configuration = configuration;
// 3、扫描Locations
mapperLocations = getResource(basePackage,XML_RESOURCE_PATTERN);
// 4、启动线程执行
exeTask();

核心类在akkad-base\persistence-mybatis\mybatis-base\src\main\java\xyz\wongs\drunkard\base\persistence\mybatis\loader\MapperAutoRefresh.java 下,而且行数太长,代码就不贴。



/**
 * Mybatis的mapper文件中的sql语句被修改后, 只能重启服务器才能被加载, 非常耗时,所以就写了一个自动加载的类,
 * 配置后检查xml文件更改,如果发生变化,重新加载xml里面的内容.
 *
 * @author <a href="https://github.com/rothschil">Sam</a>
 * @date 20/11/17 10:29
 * @since 1.0.0
 */
@Slf4j
@Component
@SuppressWarnings("unused")
public class MapperAutoRefresh implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    private static final Properties prop = new Properties();
    /**
     * 是否启用Mapper刷新线程功能
     */
    private static final boolean enabled;
    /**
     * 刷新启用后,是否启动了刷新线程
     */
    private static boolean refresh;

    /**
     * Mapper实际资源路径
     */
    private Set<String> location;
    /**
     * Mapper资源路径
     */
    private Resource[] mapperLocations;
    /**
     * MyBatis配置对象
     */
    private Configuration configuration;
    /**
     * 上一次刷新时间
     */
    private Long beforeTime = 0L;
    /**
     * 延迟刷新秒数
     */
    private static int delaySeconds;
    /**
     * 休眠时间
     */
    private static int sleepSeconds;
    /**
     * xml文件夹匹配字符串,需要根据需要修改
     */
    private static String mappingPath;

    private static final String XML_RESOURCE_PATTERN = "**/*.xml";

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    static {
        String FILE_NAME = "/conf/mybatis-refresh.properties";
        try {
            prop.load(MapperAutoRefresh.class.getResourceAsStream(FILE_NAME));
        } catch (Exception e) {
            log.error("Load mybatis-refresh “" + FILE_NAME + "” file error.");
        }

        enabled = ConstMapper.ENABLED_TRUE.equalsIgnoreCase(getPropString(ConstMapper.ENABLED));

        delaySeconds = getPropInt(ConstMapper.DELAY_SECONDS);
        sleepSeconds = getPropInt(ConstMapper.SLEEP_SECONDS);
        mappingPath = getPropString(ConstMapper.MAPPING_PATH);

        delaySeconds = delaySeconds == 0 ? 50 : delaySeconds;
        sleepSeconds = sleepSeconds == 0 ? 3 : sleepSeconds;
        mappingPath = StringUtils.isBlank(mappingPath) ? "mappings" : mappingPath;

        if(log.isDebugEnabled()){
            log.debug("[enabled] " + enabled);
            log.debug("[delaySeconds] " + delaySeconds);
            log.debug("[sleepSeconds] " + sleepSeconds);
            log.debug("[mappingPath] " + mappingPath);
        }
    }


    @PostConstruct
    public void start() throws IOException {
        SqlSessionFactory sessionFactory = applicationContext.getBean(SqlSessionFactory.class);
        this.configuration = sessionFactory.getConfiguration();
        String basePackage = "/mapper";
        mapperLocations = getResource(basePackage, XML_RESOURCE_PATTERN);
        exeTask();
    }

    /** 根据路径获取XML 的Resource
     * @param basePackage 给定包
     * @param pattern 正则表达式
     * @return javax.annotation.Resource[]
     * @date 20/11/17 10:48
     */
    public Resource[] getResource(String basePackage, String pattern) throws IOException {
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(applicationContext.getEnvironment().resolveRequiredPlaceholders(
                basePackage)) + "/" + pattern;
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        return resourcePatternResolver.getResources(packageSearchPath);
    }

    class MyBatisThreadRefresh implements Runnable {

        private final MapperAutoRefresh mapperAutoRefresh;

        MyBatisThreadRefresh(MapperAutoRefresh mapperAutoRefresh) {
            this.mapperAutoRefresh = mapperAutoRefresh;
        }

        @Override
        public void run() {
            // 解析资源
            if (null == location) {
                location = Sets.newHashSet();
                log.debug("MapperLocation's length:" + mapperLocations.length);
                for (Resource mapperLocation : mapperLocations) {
                    String s = mapperLocation.toString().replaceAll("\\\\", "/");
                    s = s.substring("file [".length(), s.lastIndexOf(mappingPath) + mappingPath.length());
                    if (!location.contains(s)) {
                        location.add(s);
                        log.info("Location:" + s);
                    }
                }
                log.info("Locarion's size:" + location.size());
            }

            // 暂定时间
            try {
                TimeUnit.SECONDS.sleep(delaySeconds);
            } catch (InterruptedException e2) {
                e2.printStackTrace();
            }

            refresh = true;
            log.info("========= Enabled refresh mybatis mapper =========");

            // 开始执行刷新操作
            while (true) {
                try {
                    for (String s : location) {
                        mapperAutoRefresh.refresh(s, beforeTime);
                    }
                    TimeUnit.SECONDS.sleep(sleepSeconds);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 执行资源刷新任务
     *
     * @date 20/11/17 11:04
     */
    public void exeTask() {
        if (null == mapperLocations || mapperLocations.length == 0) {
            return;
        }
        beforeTime = System.currentTimeMillis();
        if (enabled) {
            // 启动刷新线程
            final MapperAutoRefresh runnable = this;

            ExecutorService es = ThreadPoolsUtil.doCreate(1, 1, "Mybatis-Refresh");
            MyBatisThreadRefresh mtr = new MyBatisThreadRefresh(this);
            es.execute(mtr);
        }
    }

    /**
     * 刷新资源的操作
     *
     * @param filePath   资源的路径
     * @param beforeTime 开始时间
     * @date 20/11/17 11:06
     */
    public void refresh(String filePath, long beforeTime) {
        // 本次刷新时间
        long refrehTime = System.currentTimeMillis();
        // 获取需要刷新的Mapper文件列表
        List<File> fileList = this.getRefreshFile(new File(filePath), beforeTime);

        if (fileList.isEmpty()) {
            return;
        }
        log.info("Refresh file: " + fileList.size());

        for (File file : fileList) {
            try {
                InputStream inputStream = new FileInputStream(file);
                String resource = file.getAbsolutePath();

                // 清理原有资源,更新为自己的StrictMap方便,增量重新加载
                String[] mapFieldNames = new String[]{
                        "mappedStatements", "caches",
                        "resultMaps", "parameterMaps",
                        "keyGenerators", "sqlFragments"
                };

                for (String fieldName : mapFieldNames) {
                    Field field = configuration.getClass().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);
                    }
                }

                // 清理已加载的资源标识,方便让它重新加载。
                Field loadedResourcesField = configuration.getClass().getDeclaredField("loadedResources");
                loadedResourcesField.setAccessible(true);
                Set loadedResourcesSet = ((Set) loadedResourcesField.get(configuration));
                loadedResourcesSet.remove(resource);

                //重新编译加载资源文件。
                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(inputStream, configuration,
                        resource, configuration.getSqlFragments());
                xmlMapperBuilder.parse();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                ErrorContext.instance().reset();
            }
            if (log.isDebugEnabled()) {
                log.info("Refresh file: " + file.getAbsolutePath());
                log.info("Refresh filename: " + file.getName());
            }

            if (!fileList.isEmpty()) {
                this.beforeTime = refrehTime;
            }
        }
    }

    /**
     * 获取需要刷新的文件列表,返回 刷新文件列表
     *
     * @param dir        目录
     * @param beforeTime 上次刷新时间
     * @return java.util.List<java.io.File>
     * @date 20/11/17 11:18
     */
    private List<File> getRefreshFile(File dir, Long beforeTime) {

        List<File> fileList = new ArrayList<>();

        File[] files = dir.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    fileList.addAll(this.getRefreshFile(file, beforeTime));
                } else if (file.isFile()) {
                    if (this.checkFile(file, beforeTime)) {
                        fileList.add(file);
                    }
                } else {
                    log.error("Error file." + file.getName());
                }
            }
        }
        return fileList;
    }


    /**
     * 重写 org.apache.ibatis.session.Configuration.StrictMap 类
     * 来自 MyBatis3.4.0版本,修改 put 方法,允许反复 put更新。
     */
    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) {
            // ThinkGem 如果现在状态为刷新,则刷新(先删除后添加)
            if (MapperAutoRefresh.isRefresh()) {
                remove(key);
            }
            // ThinkGem end
            if (containsKey(key)) {
                throw new IllegalArgumentException(name + " already contains value for " + key);
            }
            if (key.contains(Constants.POINT)) {
                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 final String subject;

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

            public String getSubject() {
                return subject;
            }
        }

    }

    public static boolean isRefresh() {
        return refresh;
    }

    /**
     * 判断文件是否需要刷新,需要刷新返回true,否则返回false
     *
     * @param file       文件
     * @param beforeTime 上次刷新时间
     * @return boolean
     * @date 20/11/17 11:17
     */
    private boolean checkFile(File file, Long beforeTime) {
        return file.lastModified() > beforeTime;
    }


    /**
     * 获取整数属性
     *
     * @param key KEY
     * @return int
     * @date 20/11/17 10:37
     */
    private static int getPropInt(String key) {
        return Integer.parseInt(Objects.requireNonNull(getPropString(key)));
    }

    /**
     * 获取字符串属性
     *
     * @param key KEY
     * @return int
     * @date 20/11/17 10:37
     */
    private static String getPropString(String key) {
        return prop.getProperty(key);
    }

}

在多线程处理这块有需要注意有一定的线程使用基础,看官自行学习。

3. 源码地址,如果觉得对你有帮助,请Star

觉得对你有帮助,请Star

Github源码地址

Gitee源码地址

目录
相关文章
|
6月前
|
XML Java 数据库连接
mybatis中在xml文件中通用查询结果列如何使用
mybatis中在xml文件中通用查询结果列如何使用
405 0
|
13天前
|
SQL 缓存 Java
MyBatis如何关闭一级缓存(分注解和xml两种方式)
MyBatis如何关闭一级缓存(分注解和xml两种方式)
42 5
|
5月前
|
XML Java 数据库连接
MyBatis入门——MyBatis XML配置文件(3)
MyBatis入门——MyBatis XML配置文件(3)
71 6
|
2月前
|
SQL XML Java
mybatis :sqlmapconfig.xml配置 ++++Mapper XML 文件(sql/insert/delete/update/select)(增删改查)用法
当然,这些仅是MyBatis功能的初步介绍。MyBatis还提供了高级特性,如动态SQL、类型处理器、插件等,可以进一步提供对数据库交互的强大支持和灵活性。希望上述内容对您理解MyBatis的基本操作有所帮助。在实际使用中,您可能还需要根据具体的业务要求调整和优化SQL语句和配置。
44 1
|
3月前
|
SQL Java 数据库连接
MyBatis Mapper.XML 标签使用说明
MyBatis Mapper.XML 标签使用说明
38 0
|
5月前
|
SQL XML Java
后端数据库开发JDBC编程Mybatis之用基于XML文件的方式映射SQL语句实操
后端数据库开发JDBC编程Mybatis之用基于XML文件的方式映射SQL语句实操
71 3
|
4月前
|
XML Java 数据格式
支付系统----微信支付20---创建案例项目--集成Mybatis-plus的补充,target下只有接口的编译文件,xml文件了,添加日志的写法
支付系统----微信支付20---创建案例项目--集成Mybatis-plus的补充,target下只有接口的编译文件,xml文件了,添加日志的写法
|
5月前
|
XML Java 数据库连接
MyBatis第二课,灰度发布,@Results注解,使用xml书写mysql
MyBatis第二课,灰度发布,@Results注解,使用xml书写mysql
|
5月前
|
XML 关系型数据库 数据库
使用mybatis-generator插件生成postgresql数据库model、mapper、xml
使用mybatis-generator插件生成postgresql数据库model、mapper、xml
505 0
|
6月前
|
XML Java 数据库连接
Mybatis逆向工程的2种方法,一键高效快速生成Pojo、Mapper、XML,摆脱大量重复开发
【5月更文挑战第10天】Mybatis逆向工程的2种方法,一键高效快速生成Pojo、Mapper、XML,摆脱大量重复开发
78 6