mybatis xml文件热加载实现

本文博主给大家带来一篇 mybatis xml 文件热加载的实现教程,自博主从事开发工作使用 Mybatis 以来,如果需要修改 xml 文件的内容,通常都需要重启项目,因为不重启的话,修改是不生效的,Mybatis 仅仅会在项目初始化的时候将 xml 文件加载进内存。

本着提升开发效率且网上没有能够直接使用的轮子初衷,博主自己开发了 mybatis-xmlreload-spring-boot-starter 这个项目。它能够帮助我们在Spring Boot + Mybatis的开发环境中修改 xml 后,不需要重启项目就能让修改过后 xml 文件立即生效,实现热加载功能。这里先给出项目地址:

一、xml 文件热加载实现原理

1.1 xml 文件怎么样解析

Spring Boot + Mybatis的常规项目中,通过 org.mybatis.spring.SqlSessionFactoryBean 这个类的 buildSqlSessionFactory() 方法完成对 xml 文件的加载逻辑,这个方法只会在自动配置类 MybatisAutoConfiguration 初始化操作时进行调用。这里把 buildSqlSessionFactory() 方法中 xml 解析核心部分进行展示如下:
[图片上传失败...(image-ad4355-1679751671063)]

  1. 通过遍历 this.mapperLocations 数组(这个对象就是保存了编译后的所有 xml 文件)完成对所有 xml 文件的解析以及加载进内存。this.mapperLocations 解析逻辑在 MybatisProperties 类的 resolveMapperLocations() 方法中,它会解析 mybatis.mapperLocations 属性中的 xml 路径,将编译后的 xml 文件读取进 Resource 数组中。路径解析的核心逻辑在 PathMatchingResourcePatternResolver 类的 getResources(String locationPattern) 方法中。大家有兴趣可以自己研读一下,这里不多做介绍。
  2. 通过 xmlMapperBuilder.parse() 方法解析 xml 文件各个节点,解析方法如下:
    [图片上传失败...(image-96253e-1679751671064)]
    简单来说,这个方法会解析 xml 文件中的 mapper|resultMap|cache|cache-ref|sql|select|insert|update|delete 等标签。将他们保存在 org.apache.ibatis.session.Configuration 类的对应属性中,如下展示:
    [图片上传失败...(image-a59235-1679751671064)]
    到这里,我们就知道了 Mybatis 对 xml 文件解析是通过 xmlMapperBuilder.parse() 方法完成并且只会在项目启动时加载 xml 文件。

1.2 实现思路

通过对上述 xml 解析逻辑进行分析,我们可以通过监听 xml 文件的修改,当监听到修改操作时,直接调用 xmlMapperBuilder.parse() 方法,将修改过后的 xml 文件进行重新解析,并替换内存中的对应属性以此完成热加载操作。这里也就引出了本文所讲的主角: mybatis-xmlreload-spring-boot-starter

二、mybatis-xmlreload-spring-boot-starter 登场

mybatis-xmlreload-spring-boot-starter 这个项目完成了博主上述的实现思路,使用技术如下:

  • 修改 xml 文件的加载逻辑。在原用 mybatis-spring 中,只会加载项目编译过后的 xml 文件,也就是 target 目录下的 xml 文件。但是在mybatis-xmlreload-spring-boot-starter中,我修改了这一点,它会加载项目 resources 目录下的 xml 文件,这样对于 xml 文件的修改操作是可以立马触发热加载的。
  • 通过 io.methvin.directory-watcher 来监听 xml 文件的修改操作,它底层是通过 java.nio 的WatchService 来实现。
  • 兼容 Mybatis-plus3.0,核心代码兼容了 Mybatis-plus 自定义的 com.baomidou.mybatisplus.core.MybatisConfiguration 类,任然可以使用 xml 文件热加载功能。

2.1 核心代码

项目的结构如下:
[图片上传失败...(image-7add8e-1679751671064)]
核心代码在 MybatisXmlReload 类中,代码展示:

/**
 * mybatis-xml-reload核心xml热加载逻辑
 */
public class MybatisXmlReload {
    private static final Logger logger = LoggerFactory.getLogger(MybatisXmlReload.class);
    /**
     * 是否启动以及xml路径的配置类
     */
    private MybatisXmlReloadProperties prop;
    /**
     * 获取项目中初始化完成的SqlSessionFactory列表,对多数据源进行处理
     */
    private List<SqlSessionFactory> sqlSessionFactories;
    public MybatisXmlReload(MybatisXmlReloadProperties prop, List<SqlSessionFactory> sqlSessionFactories) {
        this.prop = prop;
        this.sqlSessionFactories = sqlSessionFactories;
    }
    public void xmlReload() throws IOException {
        PathMatchingResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();
        String CLASS_PATH_TARGET = File.separator + "target" + File.separator + "classes";
        String MAVEN_RESOURCES = "/src/main/resources";
        // 1. 解析项目所有xml路径,获取xml文件在target目录中的位置
        List<Resource> mapperLocationsTmp = Stream.of(Optional.of(prop.getMapperLocations()).orElse(new String[0]))
                .flatMap(location -> Stream.of(getResources(patternResolver, location))).toList();

        List<Resource> mapperLocations = new ArrayList<>(mapperLocationsTmp.size() * 2);
        Set<Path> locationPatternSet = new HashSet<>();
        // 2. 根据xml文件在target目录下的位置,进行路径替换找到该xml文件在resources目录下的位置
        for (Resource mapperLocation : mapperLocationsTmp) {
            mapperLocations.add(mapperLocation);
            String absolutePath = mapperLocation.getFile().getAbsolutePath();
            File tmpFile = new File(absolutePath.replace(CLASS_PATH_TARGET, MAVEN_RESOURCES));
            if (tmpFile.exists()) {
                locationPatternSet.add(Path.of(tmpFile.getParent()));
                FileSystemResource fileSystemResource = new FileSystemResource(tmpFile);
                mapperLocations.add(fileSystemResource);
            }
        }
        // 3. 对resources目录的xml文件修改进行监听
        List<Path> rootPaths = new ArrayList<>();
        rootPaths.addAll(locationPatternSet);
        DirectoryWatcher watcher = DirectoryWatcher.builder()
                .paths(rootPaths) // or use paths(directoriesToWatch)
                .listener(event -> {
                    switch (event.eventType()) {
                        case CREATE: /* file created */
                            break;
                        case MODIFY: /* file modified */
                            Path modifyPath = event.path();
                            String absolutePath = modifyPath.toFile().getAbsolutePath();
                            logger.info("mybatis xml file has changed:" + modifyPath);
                            // 4. 对多个数据源进行遍历,判断修改过的xml文件属于那个数据源
                            for (SqlSessionFactory sqlSessionFactory : sqlSessionFactories) {
                                try {
                                    // 5. 获取Configuration对象
                                    Configuration targetConfiguration = sqlSessionFactory.getConfiguration();
                                    Class<?> tClass = targetConfiguration.getClass(), aClass = targetConfiguration.getClass();
                                    if (targetConfiguration.getClass().getSimpleName().equals("MybatisConfiguration")) {
                                        aClass = Configuration.class;
                                    }
                                    Set<String> loadedResources = (Set<String>) getFieldValue(targetConfiguration, aClass, "loadedResources");
                                    loadedResources.clear();

                                    Map<String, ResultMap> resultMaps = (Map<String, ResultMap>) getFieldValue(targetConfiguration, tClass, "resultMaps");
                                    Map<String, XNode> sqlFragmentsMaps = (Map<String, XNode>) getFieldValue(targetConfiguration, tClass, "sqlFragments");
                                    Map<String, MappedStatement> mappedStatementMaps = (Map<String, MappedStatement>) getFieldValue(targetConfiguration, tClass, "mappedStatements");
                                    // 6. 遍历xml文件
                                    for (Resource mapperLocation : mapperLocations) {
                                        // 7. 判断是否是被修改过的xml文件,否则跳过
                                        if (!absolutePath.equals(mapperLocation.getFile().getAbsolutePath())) {
                                            continue;
                                        }
                                        // 8. 重新解析xml文件,替换Configuration对象的相对应属性
                                        XPathParser parser = new XPathParser(mapperLocation.getInputStream(), true, targetConfiguration.getVariables(), new XMLMapperEntityResolver());
                                        XNode mapperXnode = parser.evalNode("/mapper");
                                        List<XNode> resultMapNodes = mapperXnode.evalNodes("/mapper/resultMap");
                                        String namespace = mapperXnode.getStringAttribute("namespace");
                                        for (XNode xNode : resultMapNodes) {
                                            String id = xNode.getStringAttribute("id", xNode.getValueBasedIdentifier());
                                            resultMaps.remove(namespace + "." + id);
                                        }

                                        List<XNode> sqlNodes = mapperXnode.evalNodes("/mapper/sql");
                                        for (XNode sqlNode : sqlNodes) {
                                            String id = sqlNode.getStringAttribute("id", sqlNode.getValueBasedIdentifier());
                                            sqlFragmentsMaps.remove(namespace + "." + id);
                                        }

                                        List<XNode> msNodes = mapperXnode.evalNodes("select|insert|update|delete");
                                        for (XNode msNode : msNodes) {
                                            String id = msNode.getStringAttribute("id", msNode.getValueBasedIdentifier());
                                            mappedStatementMaps.remove(namespace + "." + id);
                                        }
                                        try {
                                            // 9. 重新加载和解析被修改的 xml 文件
                                            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                                                    targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
                                            xmlMapperBuilder.parse();
                                        } catch (Exception e) {
                                            logger.error(e.getMessage(), e);
                                        }
                                        logger.info("Parsed mapper file: '" + mapperLocation + "'");
                                    }
                                } catch (Exception e) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            break;
                        case DELETE: /* file deleted */
                            break;
                    }
                })
                .build();
        ThreadFactory threadFactory = r -> {
            Thread thread = new Thread(r);
            thread.setName("xml-reload");
            thread.setDaemon(true);
            return thread;
        };
        watcher.watchAsync(new ScheduledThreadPoolExecutor(1, threadFactory));
    }

    /**
     * 根据xml路径获取对应实际文件
     *
     * @param location 文件位置
     * @return Resource[]
     */
    private Resource[] getResources(PathMatchingResourcePatternResolver patternResolver, String location) {
        try {
            return patternResolver.getResources(location);
        } catch (IOException e) {
            return new Resource[0];
        }
    }

    /**
     * 根据反射获取 Configuration 对象中属性
     */
    private static Object getFieldValue(Configuration targetConfiguration, Class<?> aClass,
                                        String filed) throws NoSuchFieldException, IllegalAccessException {
        Field resultMapsField = aClass.getDeclaredField(filed);
        resultMapsField.setAccessible(true);
        return resultMapsField.get(targetConfiguration);
    }
}

代码执行逻辑:

  1. 解析配置文件指定的 xml 路径,获取 xml 文件在 target 目录下的位置
  2. 根据 xml 文件在 target 目录下的位置,进行路径替换找到 xml 文件所在 resources 目录下的位置
  3. 对 resources 目录的 xml 文件的修改操作进行监听
  4. 对多个数据源进行遍历,判断修改过的 xml 文件属于那个数据源
  5. 根据 Configuration 对象获取对应的标签属性
  6. 遍历 resources 目录下 xml 文件列表
  7. 判断是否是被修改过的 xml 文件,否则跳过
  8. 解析被修改的 xml 文件,替换 Configuration 对象中的相对应属性
  9. 重新加载和解析被修改的 xml 文件

2.2 安装方式

  • Spring Boot3.0 中,当前博主提供了mybatis-xmlreload-spring-boot-starter在 Maven 项目中的坐标地址如下
<dependency>
    <groupId>com.wayn</groupId>
    <artifactId>mybatis-xmlreload-spring-boot-starter</artifactId>
    <version>3.0.3.m1</version>
</dependency>
  • Spring Boot2.0 Maven 项目中的坐标地址如下
<dependency>
    <groupId>com.wayn</groupId>
    <artifactId>mybatis-xmlreload-spring-boot-starter</artifactId>
    <version>2.0.1.m1</version>
</dependency>

2.3 使用配置

Maven 项目写入mybatis-xmlreload-spring-boot-starter坐标后即可使用本项目功能,默认是不启用 xml 文件的热加载功能,想要开启的话通过在项目配置文件中设置 mybatis-xml-reload.enabled 为 true,并指定 mybatis-xml-reload.mapper-locations 属性,也就是 xml 文件位置即可启动。具体配置如下:

# mybatis xml文件热加载配置
mybatis-xml-reload:
  # 是否开启 xml 热更新,true开启,false不开启,默认为false
  enabled: true 
  # xml文件位置,eg: `classpath*:mapper/**/*Mapper.xml,classpath*:other/**/*Mapper.xml`
  mapper-locations: classpath:mapper/*Mapper.xml

三、最后

欢迎大家使用mybatis-xmlreload-spring-boot-starter,使用中遇到问题可以提交 issue 或者加博主私人微信waynaqua给你解决。 再附项目地址:

希望这个项目能够提升大家的日常开发效率,节约重启次数,喜欢的朋友们可以点赞加关注😘。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容