您当前的位置:首页 > 电脑百科 > 程序开发 > 框架

mybatis xml文件热加载实现

时间:2023-03-25 14:33:29  来源:今日头条  作者:wayn111

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

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

  • https://Github.com/wayn111/mybatis-xmlreload-spring-boot-starter 欢迎大家关注,点个star
  • 博主github:https://github.com/wayn111

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

1.1 xml 文件怎么样解析

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

 

  1. 通过遍历 this.mApperLocations 数组(这个对象就是保存了编译后的所有 xml 文件)完成对所有 xml 文件的解析以及加载进内存。this.mapperLocations 解析逻辑在 MybatisProperties 类的 resolveMapperLocations() 方法中,它会解析 mybatis.mapperLocations 属性中的 xml 路径,将编译后的 xml 文件读取进 Resource 数组中。路径解析的核心逻辑在 PathMatchingResourcePatternResolver 类的 getResources(String locationPattern) 方法中。大家有兴趣可以自己研读一下,这里不多做介绍。
  2. 通过 xmlMapperBuilder.parse() 方法解析 xml 文件各个节点,解析方法如下: 简单来说,这个方法会解析 xml 文件中的 mapper|resultMap|cache|cache-ref|sql|select|insert|update|delete 等标签。将他们保存在 org.Apache.ibatis.session.Configuration 类的对应属性中,如下展示: 到这里,我们就知道了 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 核心代码

项目的结构如下:

 

核心代码在 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」给你解决。 再附项目地址:

  • https://github.com/wayn111/mybatis-xmlreload-spring-boot-starter

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



Tags:mybatis   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Mybatis参数映射搞不明白?来试试这个工具吧!
之前在《使用技巧-Mybatis参数映射》《使用技巧-Mybatis参数映射(2)》提到了Mybatis的一些参数映射技巧,但是平时使用的时候有些小伙伴可能不知道自己写的#{}表达式能不能获取...【详细内容】
2024-02-28  Search: mybatis  点击:(33)  评论:(0)  加入收藏
如何在Spring项目中配置MP(MyBatis-Plus)集成?
在Spring项目中集成MP,需要进行以下配置:1. 引入依赖:在项目的pom.xml文件中添加MP相关依赖,例如:```xml<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plu...【详细内容】
2024-01-09  Search: mybatis  点击:(87)  评论:(0)  加入收藏
Mybatis占位符#和$的区别?源码解读
本文针对笔者日常开发中对 Mybatis 占位符 #{} 和 ${} 使用时机结合源码,思考总结而来 &bull; Mybatis 版本 3.5.11 &bull; Spring boot 版本 3.0.2 &bull; mybatis-spring...【详细内容】
2023-10-27  Search: mybatis  点击:(399)  评论:(0)  加入收藏
看完这篇文章,你也可以手写MyBatis部分源码(JDBC)
一、持久化机制持久化(persistence): 把数据保存到可调电式存储设备中以供之后使用。大多数情况下,特别是企业级应用,数据持久化意味着将内存中的数据保存到硬盘上加以”固化...【详细内容】
2023-10-09  Search: mybatis  点击:(332)  评论:(0)  加入收藏
Mybatis-Flex初体验
本篇文章内容主要包括: MyBatis-Flex 介绍MyBatis-Flex 是一个优雅的 MyBatis 增强框架,它非常轻量、同时拥有极高的性能与灵活性。我们可以轻松的使用 Mybaits-Flex 链接任何...【详细内容】
2023-09-24  Search: mybatis  点击:(196)  评论:(0)  加入收藏
MyBatis简单易用的背后隐藏的挑战
MyBatis,作为一款备受欢迎的持久层框架,它的简单易用以及灵活的配置吸引了无数的开发者。然而,随着项目的不断发展,规模的逐渐扩大,MyBatis的一些挑战也开始逐渐浮出水面。首先,由...【详细内容】
2023-09-15  Search: mybatis  点击:(242)  评论:(0)  加入收藏
MyBatis缓存机制
MyBatis 的缓存机制属于本地缓存,适用于单机系统,它的作用是减少数据库的查询次数,提高系统性能。MyBaits 中包含两级本地缓存: 一级缓存:SqlSession 级别的,是 MyBatis 自带的缓...【详细内容】
2023-09-12  Search: mybatis  点击:(229)  评论:(0)  加入收藏
对比 MyBatis 和 MyBatis-Plus 批量插入、批量更新的性能和区别
1 环境准备1.1 搭建 MyBatis-Plus 环境 创建 maven springboot 工程 导入依赖:web 启动器、jdbc、、java 连接 mysql、Lombok、druid 连接池启动器、mybatis-plus 启动器 编...【详细内容】
2023-09-08  Search: mybatis  点击:(191)  评论:(0)  加入收藏
Spring Data JPA 和 MyBatis 谁更强?
我无法明确的告诉你JPA和MyBatis在国内哪个会更流行,我本人更喜欢JPA,但是我本人日常开发用MyBatis多。但是我的回答绝对不是在划水,而是我多年来自己的一点小小的思考。MyBati...【详细内容】
2023-08-22  Search: mybatis  点击:(335)  评论:(0)  加入收藏
Mybatis-Plus可能会导致数据库死锁
一、场景还原1.版本信息MySQL版本:5.6.36-82.1-logMybatis-Plus的starter版本:3.3.2存储引擎:InnoDB2.死锁现象A同学在生产环境使用了Mybatis-Plus提供的com.baomidou.mybatisp...【详细内容】
2023-08-14  Search: mybatis  点击:(171)  评论:(0)  加入收藏
▌简易百科推荐
Web Components实践:如何搭建一个框架无关的AI组件库
一、让人又爱又恨的Web ComponentsWeb Components是一种用于构建可重用的Web元素的技术。它允许开发者创建自定义的HTML元素,这些元素可以在不同的Web应用程序中重复使用,并且...【详细内容】
2024-04-03  京东云开发者    Tags:Web Components   点击:(8)  评论:(0)  加入收藏
Kubernetes 集群 CPU 使用率只有 13% :这下大家该知道如何省钱了
作者 | THE STACK译者 | 刘雅梦策划 | Tina根据 CAST AI 对 4000 个 Kubernetes 集群的分析,Kubernetes 集群通常只使用 13% 的 CPU 和平均 20% 的内存,这表明存在严重的过度...【详细内容】
2024-03-08  InfoQ    Tags:Kubernetes   点击:(12)  评论:(0)  加入收藏
Spring Security:保障应用安全的利器
SpringSecurity作为一个功能强大的安全框架,为Java应用程序提供了全面的安全保障,包括认证、授权、防护和集成等方面。本文将介绍SpringSecurity在这些方面的特性和优势,以及它...【详细内容】
2024-02-27  风舞凋零叶    Tags:Spring Security   点击:(54)  评论:(0)  加入收藏
五大跨平台桌面应用开发框架:Electron、Tauri、Flutter等
一、什么是跨平台桌面应用开发框架跨平台桌面应用开发框架是一种工具或框架,它允许开发者使用一种统一的代码库或语言来创建能够在多个操作系统上运行的桌面应用程序。传统上...【详细内容】
2024-02-26  贝格前端工场    Tags:框架   点击:(47)  评论:(0)  加入收藏
Spring Security权限控制框架使用指南
在常用的后台管理系统中,通常都会有访问权限控制的需求,用于限制不同人员对于接口的访问能力,如果用户不具备指定的权限,则不能访问某些接口。本文将用 waynboot-mall 项目举例...【详细内容】
2024-02-19  程序员wayn  微信公众号  Tags:Spring   点击:(39)  评论:(0)  加入收藏
开发者的Kubernetes懒人指南
你可以将本文作为开发者快速了解 Kubernetes 的指南。从基础知识到更高级的主题,如 Helm Chart,以及所有这些如何影响你作为开发者。译自Kubernetes for Lazy Developers。作...【详细内容】
2024-02-01  云云众生s  微信公众号  Tags:Kubernetes   点击:(50)  评论:(0)  加入收藏
链世界:一种简单而有效的人类行为Agent模型强化学习框架
强化学习是一种机器学习的方法,它通过让智能体(Agent)与环境交互,从而学习如何选择最优的行动来最大化累积的奖励。强化学习在许多领域都有广泛的应用,例如游戏、机器人、自动驾...【详细内容】
2024-01-30  大噬元兽  微信公众号  Tags:框架   点击:(68)  评论:(0)  加入收藏
Spring实现Kafka重试Topic,真的太香了
概述Kafka的强大功能之一是每个分区都有一个Consumer的偏移值。该偏移值是消费者将读取的下一条消息的值。可以自动或手动增加该值。如果我们由于错误而无法处理消息并想重...【详细内容】
2024-01-26  HELLO程序员  微信公众号  Tags:Spring   点击:(86)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19   Java中文社群  微信公众号  Tags:SpringBoot   点击:(86)  评论:(0)  加入收藏
花 15 分钟把 Express.js 搞明白,全栈没有那么难
Express 是老牌的 Node.js 框架,以简单和轻量著称,几行代码就可以启动一个 HTTP 服务器。市面上主流的 Node.js 框架,如 Egg.js、Nest.js 等都与 Express 息息相关。Express 框...【详细内容】
2024-01-16  程序员成功  微信公众号  Tags:Express.js   点击:(88)  评论:(0)  加入收藏
站内最新
站内热门
站内头条