乐趣区

关于后端:mybatis-xml文件热加载实现

本文博主给大家带来一篇 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

心愿这个我的项目可能晋升大家的日常开发效率,节约重启次数,喜爱的敌人们能够点赞加关注😘。

退出移动版