自我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)熱加載功能吃沪。這里給出項目地址:
- https://github.com/wayn111/mybatis-xmlreload-spring-boot-starter 歡迎大家關(guān)注汤善,點個star
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í)行邏輯:
- 通過項目初始化時傳入
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;
}
- 解析配置文件指定的 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();
- 根據(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);
}
}
- 對 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));
- 對多個數(shù)據(jù)源進(jìn)行遍歷,判斷修改過的 xml 文件屬于那個數(shù)據(jù)源
// 對多個數(shù)據(jù)源進(jìn)行遍歷瞻离,判斷修改過的xml文件屬于那個數(shù)據(jù)源
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactories) {
...
}
- 根據(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");
- 遍歷 resources 目錄下 xml 文件列表
// 遍歷 resources 目錄下 xml 文件列表
for (Resource mapperLocation : mapperLocations) {
...
}
- 判斷是否是被修改過的 xml 文件腾仅,否則跳過
// 判斷是否是被修改過的xml文件,否則跳過
if (!absolutePath.equals(mapperLocation.getFile().getAbsolutePath())) {
continue;
}
- 解析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);
}
- 重新加載和解析被修改的 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)注联喘,你的支持將是我更新動力??叭喜。