天天看點

分享一個修改了xml檔案再也不用重新開機的項目mybatis-xmlrelaod

作者:wayn111

自我18年使用 Mybaits 以來,開發環境中如果修改了 xml 檔案後,隻有重新開機項目才能生效,如果小項目重新開機還好,但是對于一個重新開機需要十幾分鐘的大型項目來說,這就非常耗時了。開發人員因為修改了xml 檔案少量内容,比如添加一個逗号、查詢增加一個字段或者修改一個 bug 等,就需要重新開機整個項目,這就非常痛苦了。

是以在這裡給大家推薦一個實作了 Mybatis xml檔案熱加載的項目,「mybatis-xmlreload-spring-boot-starter」。它能夠幫助我們在「Spring Boot + Mybatis」的開發環境中修改 xml 後,不需要重新開機項目就能讓修改過後 xml 檔案立即生效,實作熱加載功能。這裡給出項目位址:

  • https://github.com/wayn111/mybatis-xmlreload-spring-boot-starter 歡迎大家關注,點個star

ps:「mybatis-xmlreload-spring-boot-starter」目前 3.0.3.m1 版本實作了 xml 檔案修改已有内容,比如修改 sql 語句、添加查詢字段、添加查詢條件等,可以實作熱加載功能。但是對于 xml 檔案添加 insert|update|delete|select 标簽等内容後,是無法實作熱加載的。衆所周知,在 Idea 環境進行 Java 開發,在方法内修改方法内容是可以熱加載的。但是添加新方法、添加方法參數,修改方法參數,修改方法傳回值等都是無法直接熱加載的。

一、mybatis-xmlreload-spring-boot-starter使用

「mybatis-xmlreload-spring-boot-starter」原理:

  • 修改 xml 檔案的加載邏輯。在普通的 mybatis-spring 項目中,預設隻會附加元件目編譯過後的 xml 檔案,也就是 target 目錄下的 xml 檔案。但是在「mybatis-xmlreload-spring-boot-starter」中,修改了這一點,它會附加元件目 resources 目錄下的 xml 檔案,這樣使用者對于 resources 目錄下 xml 檔案的修改操作是可以立即觸發熱加載的。
  • 通過 io.methvin.directory-watcher 項目來監聽 xml 檔案的修改操作,它底層是通過 java.nio 的WatchService 來實作,當我們監聽了整個 resources 目錄後,xml 檔案的修改會立馬觸發 MODIFY 事件。
  • 通過 mybatis-spring 項目原生的 xmlMapperBuilder.parse() 方法重新加載解析修改過後的 xml 檔案來保證項目對于 Mybatis 的相容性處理。

二、技術原理

「mybatis-xmlreload-spring-boot-starter」代碼結構如下:

分享一個修改了xml檔案再也不用重新開機的項目mybatis-xmlrelaod

核心代碼在「MybatisXmlReload」類中,執行邏輯:

  1. 通過項目初始化時傳入 MybatisXmlReloadProperties prop, List<SqlSessionFactory> sqlSessionFactories 參數,擷取「mybatis-xmlreload-spring-boot-starter」的配置資訊,以及項目中的資料源配置
/**
 * 是否啟動以及xml路徑的配置類
 */
private MybatisXmlReloadProperties prop;
/**
 * 擷取項目中初始化完成的SqlSessionFactory清單,對多資料源進行處理
 */
private List<SqlSessionFactory> sqlSessionFactories;
public MybatisXmlReload(MybatisXmlReloadProperties prop, 
        List<SqlSessionFactory> sqlSessionFactories) {
    this.prop = prop;
    this.sqlSessionFactories = sqlSessionFactories;
}           
  1. 解析配置檔案指定的 xml 路徑,擷取 xml 檔案在 target 目錄下的位置
// 解析項目所有xml路徑,擷取xml檔案在target目錄中的位置
List<Resource> mapperLocationsTmp = Stream.of(
  Optional.of(prop.getMapperLocations())
  .orElse(new String[0]))
  .flatMap(location -> Stream.of(getResources(patternResolver, location)))
  .toList();           
  1. 根據 xml 檔案在 target 目錄下的位置,進行路徑替換找到 xml 檔案所在 resources 目錄下的位置
// 根據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);
    }
}           
  1. 對 resources 目錄的 xml 檔案的修改操作進行監聽
// 對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);
                // 執行熱加載邏輯...
                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));           
  1. 對多個資料源進行周遊,判斷修改過的 xml 檔案屬于那個資料源
// 對多個資料源進行周遊,判斷修改過的xml檔案屬于那個資料源
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactories) {
    ...
}           
  1. 根據 Configuration 對象擷取對應的标簽屬性
// 根據 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");           
  1. 周遊 resources 目錄下 xml 檔案清單
// 周遊 resources 目錄下 xml 檔案清單
for (Resource mapperLocation : mapperLocations) {
    ...
}           
  1. 判斷是否是被修改過的 xml 檔案,否則跳過
// 判斷是否是被修改過的xml檔案,否則跳過
if (!absolutePath.equals(mapperLocation.getFile().getAbsolutePath())) {
    continue;
}           
  1. 解析xml檔案,擷取修改後的xml檔案标簽對應的 resultMaps|sqlFragmentsMaps|mappedStatementMaps 的屬性并執行替換邏輯,并且相容 mybatis-plus 的替換邏輯
// 重新解析xml檔案,替換Configuration對象的相對應屬性
XPathParser parser = new XPathParser(mapperLocation.getInputStream(), 
    true, 
    targetConfiguration.getVariables(), 
    new XMLMapperEntityResolver());
XNode mapperXnode = parser.evalNode("/mapper");
String namespace = mapperXnode.getStringAttribute("namespace");
List<XNode> resultMapNodes = mapperXnode.evalNodes("/mapper/resultMap");
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);
}           
  1. 重新加載和解析被修改的 xml 檔案
// 9. 重新加載和解析被修改的 xml 檔案
try {
    XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(
        mapperLocation.getInputStream(),
        targetConfiguration,
        mapperLocation.toString(), 
        targetConfiguration.getSqlFragments());
    xmlMapperBuilder.parse();
} catch (Exception e) {
    logger.error(e.getMessage(), e);
}           

三、安裝方式

  • 在 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>           

四、使用配置

「mybatis-xmlreload-spring-boot-starter」 目前隻有兩個配置屬性。mybatis-xml-reload.enabled 預設是 false, 也就是不啟用 xml 檔案的熱加載功能,想要開啟的話通過在項目配置檔案中設定 mybatis-xml-reload.enabled 為 true。還有一個配置屬性是 mybatis-xml-reload.mapper-locations,執行熱加載的 xml 檔案路徑,這個屬性需要手動填寫,跟項目中的 mybatis.mapper-locations 保持一直即可。具體配置如下:

# 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。送出的問題我都會一一檢視并回複。再附項目位址:

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

最後再說一句,感興趣的朋友可以點贊加關注,你的支援将是我更新動力。

繼續閱讀