分享一個修改了xml文件再也不用重啟的項目mybatis-xmlrealod

自我18年使用 Mybaits 以來,開發(fā)環(huán)境中如果修改了 xml 文件后,只有重啟項目才能生效地淀,如果小項目重啟還好偿渡,但是對于一個重啟需要十幾分鐘的大型項目來說臼寄,這就非常耗時了。開發(fā)人員因為修改了xml 文件少量內(nèi)容溜宽,比如添加一個逗號脯厨、查詢增加一個字段或者修改一個 bug 等,就需要重啟整個項目坑质,這就非常痛苦了合武。

所以在這里給大家推薦一個實現(xiàn)了 Mybatis xml文件熱加載的項目,mybatis-xmlreload-spring-boot-starter涡扼。它能夠幫助我們在Spring Boot + Mybatis的開發(fā)環(huán)境中修改 xml 后稼跳,不需要重啟項目就能讓修改過后 xml 文件立即生效,實現(xiàn)熱加載功能吃沪。這里給出項目地址:

ps:mybatis-xmlreload-spring-boot-starter目前 3.0.3.m1 版本實現(xiàn)了 xml 文件修改已有內(nèi)容,比如修改 sql 語句票彪、添加查詢字段红淡、添加查詢條件等,可以實現(xiàn)熱加載功能降铸。但是對于 xml 文件添加 insert|update|delete|select 標(biāo)簽等內(nèi)容后在旱,是無法實現(xiàn)熱加載的。眾所周知推掸,在 Idea 環(huán)境進(jìn)行 Java 開發(fā)桶蝎,在方法內(nèi)修改方法內(nèi)容是可以熱加載的。但是添加新方法谅畅、添加方法參數(shù)登渣,修改方法參數(shù),修改方法返回值等都是無法直接熱加載的毡泻。

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

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

  • 修改 xml 文件的加載邏輯。在普通的 mybatis-spring 項目中仇味,默認(rèn)只會加載項目編譯過后的 xml 文件呻顽,也就是 target 目錄下的 xml 文件。但是在mybatis-xmlreload-spring-boot-starter中邪铲,修改了這一點芬位,它會加載項目 resources 目錄下的 xml 文件无拗,這樣用戶對于 resources 目錄下 xml 文件的修改操作是可以立即觸發(fā)熱加載的。
  • 通過 io.methvin.directory-watcher 項目來監(jiān)聽 xml 文件的修改操作被饿,它底層是通過 java.nio 的WatchService 來實現(xiàn)搪搏,當(dāng)我們監(jiān)聽了整個 resources 目錄后,xml 文件的修改會立馬觸發(fā) MODIFY 事件疯溺。
  • 通過 mybatis-spring 項目原生的 xmlMapperBuilder.parse() 方法重新加載解析修改過后的 xml 文件來保證項目對于 Mybatis 的兼容性處理论颅。

二、技術(shù)原理

mybatis-xmlreload-spring-boot-starter代碼結(jié)構(gòu)如下:
[圖片上傳失敗...(image-758d9a-1679839521956)]
核心代碼在MybatisXmlReload類中囱嫩,執(zhí)行邏輯:

  1. 通過項目初始化時傳入 MybatisXmlReloadProperties prop, List<SqlSessionFactory> sqlSessionFactories 參數(shù)恃疯,獲取mybatis-xmlreload-spring-boot-starter的配置信息,以及項目中的數(shù)據(jù)源配置
/**
 * 是否啟動以及xml路徑的配置類
 */
private MybatisXmlReloadProperties prop;
/**
 * 獲取項目中初始化完成的SqlSessionFactory列表墨闲,對多數(shù)據(jù)源進(jìn)行處理
 */
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. 根據(jù) xml 文件在 target 目錄下的位置鸳碧,進(jìn)行路徑替換找到 xml 文件所在 resources 目錄下的位置
// 根據(jù)xml文件在target目錄下的位置盾鳞,進(jìn)行路徑替換找到該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 文件的修改操作進(jìn)行監(jiān)聽
// 對resources目錄的xml文件修改進(jìn)行監(jiān)聽
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);
                // 執(zhí)行熱加載邏輯...
                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. 對多個數(shù)據(jù)源進(jìn)行遍歷,判斷修改過的 xml 文件屬于那個數(shù)據(jù)源
// 對多個數(shù)據(jù)源進(jìn)行遍歷瞻离,判斷修改過的xml文件屬于那個數(shù)據(jù)源
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactories) {
    ...
}
  1. 根據(jù) Configuration 對象獲取對應(yīng)的標(biāo)簽屬性
// 根據(jù) Configuration 對象獲取對應(yīng)的標(biāo)簽屬性
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文件標(biāo)簽對應(yīng)的 resultMaps|sqlFragmentsMaps|mappedStatementMaps 的屬性并執(zhí)行替換邏輯攒砖,并且兼容 mybatis-plus 的替換邏輯
// 重新解析xml文件,替換Configuration對象的相對應(yīng)屬性
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 項目提供坐標(biāo)地址如下:
<dependency>
    <groupId>com.wayn</groupId>
    <artifactId>mybatis-xmlreload-spring-boot-starter</artifactId>
    <version>3.0.3.m1</version>
</dependency>
  • Spring Boot2.0 Maven 項目提供坐標(biāo)地址如下:
<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 默認(rèn)是 false, 也就是不啟用 xml 文件的熱加載功能亿卤,想要開啟的話通過在項目配置文件中設(shè)置 mybatis-xml-reload.enabled 為 true秆乳。還有一個配置屬性是 mybatis-xml-reload.mapper-locations,執(zhí)行熱加載的 xml 文件路徑双藕,這個屬性需要手動填寫忧陪,跟項目中的 mybatis.mapper-locations 保持一直即可。具體配置如下:

# mybatis xml文件熱加載配置
mybatis-xml-reload:
  # 是否開啟 xml 熱更新叶堆,true開啟,false不開啟忘渔,默認(rèn)為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勺美。提交的問題我都會一一查看并回復(fù)。再附項目地址:

最后再說一句,感興趣的朋友可以點贊加關(guān)注联喘,你的支持將是我更新動力??叭喜。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末闪幽,一起剝皮案震驚了整個濱河市盯腌,隨后出現(xiàn)的幾起案子着倾,更是在濱河造成了極大的恐慌,老刑警劉巖崇决,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異睁枕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)跳仿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門山上,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鸯屿,你說我怎么就攤上這事∩裟眨” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蕉饼。 經(jīng)常有香客問我擎椰,道長瓤的,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任糯俗,我火速辦了婚禮得湘,結(jié)果婚禮上杖玲,老公的妹妹穿的比我還像新娘。我一直安慰自己淘正,他們只是感情好摆马,可當(dāng)我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鸿吆,像睡著了一般囤采。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上激蹲,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天差油,我揣著相機(jī)與錄音钱骂,去河邊找鬼。 笑死省核,一個胖子當(dāng)著我的面吹牛脓匿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播身辨,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼杂曲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起诈胜,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后毡们,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體框咙,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡悔据,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年逝段,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帚桩。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡亿驾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出账嚎,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布像寒,位于F島的核電站丁鹉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏召锈。R本人自食惡果不足惜旁振,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拐袜,春花似錦吉嚣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至甜攀,卻和暖如春秋泄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背规阀。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工印衔, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人姥敛。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓奸焙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親彤敛。 傳聞我的和親對象是個殘疾皇子与帆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容