Mybatis中mapper.xml實(shí)現(xiàn)熱加載介紹
背景
有些需求可能更新sql的頻率較高,但又不想頻繁發(fā)布java應(yīng)用程序,所以mybatis-mapper.xml熱加載的需求順勢而出。
目的
只需調(diào)起加載mapper.xml的程序,無需重啟整個(gè)java應(yīng)用,低耦合。
實(shí)現(xiàn)方式
mapper.xml可以指定路徑。如springboot工程resources目錄下;亦可獨(dú)立維護(hù)在某個(gè)git倉庫,然后由程序加載到運(yùn)行機(jī)器上去。
具體加載git倉庫到運(yùn)行機(jī)器代碼如下:
package com.jason.git;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.PullResult;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
import java.io.File;
import java.io.IOException;
/**
* @author jason
* @create 2022/1/19 11:39 上午
**/
@Repository
public class GitConfigRepository {
private static final long MIN_CHECKOUT_INTERVAL = 1000L * 10;
private static final long MAX_LOCAL_LIFE_CYCLE = 2 * 86400 * 1000L;
private Log log = LogFactory.getLog(GitConfigRepository.class);
@Value("${git.repository:}")
private String gitRepositoryURL;
@Value("${git.branch:master}")
private String gitBranch;
@Value("${git.username:}")
private String gitUsername;
@Value("${git.password:}")
private String gitPassword;
@Value("${bi.meta.git.localRepository:}")
private String localRepository;
private long lastCheckoutTimestamp;
private long localRepositoryTimestamp;
private File gitDir;
private Git git;
public File getRepositoryDir() throws IOException, GitAPIException {
long now = System.currentTimeMillis();
if (now - lastCheckoutTimestamp > MIN_CHECKOUT_INTERVAL) {
this.lastCheckoutTimestamp = now;
if (StringUtils.isNotEmpty(localRepository)) {
gitDir = new File(localRepository);
} else {
boolean isNewDir = false;
if (gitDir != null && !gitDir.exists()) {
gitDir = null;
}
if (gitDir != null) {
if (now - localRepositoryTimestamp > MAX_LOCAL_LIFE_CYCLE) {
localRepositoryTimestamp = 0;
try {
gitDir.delete();
} catch (Exception e) {
// do nothing
}
gitDir = null;
}
File keyFile = new File(gitDir, "global/config/config.yml");
if (!(keyFile.exists() && keyFile.length() > 0)) {
try {
gitDir.delete();
} catch (Exception e) {
// do nothing
}
gitDir = null;
}
}
if (gitDir == null) {
gitDir = File.createTempFile("egret-meta", ".git");
if (!gitDir.delete()) {
throw new IOException("無法刪除臨時(shí)文件: " + gitDir.getAbsolutePath());
}
if (!gitDir.mkdir()) {
throw new IOException("創(chuàng)建歷史Git本地目錄失敗: " + gitDir.getAbsolutePath());
}
gitDir.deleteOnExit();
isNewDir = true;
localRepositoryTimestamp = now;
}
if (StringUtils.isNotEmpty(gitRepositoryURL)) {
//設(shè)置遠(yuǎn)程服務(wù)器上的用戶名和密碼
UsernamePasswordCredentialsProvider usernamePasswordCredentialsProvider = new
UsernamePasswordCredentialsProvider(gitUsername, gitPassword);
if (isNewDir) {
//克隆代碼庫命令
CloneCommand cloneCommand = Git.cloneRepository();
git = cloneCommand.setURI(gitRepositoryURL)
.setBranch(gitBranch)
.setDirectory(gitDir)
.setCredentialsProvider(usernamePasswordCredentialsProvider)
.call();
}
log.info("Checkout meta configs from. [" + gitRepositoryURL + "|" + gitBranch + "]");
PullResult call = git.pull().setRemoteBranchName(gitBranch).setCredentialsProvider(usernamePasswordCredentialsProvider).call();
log.info("Checkout meta configs OK.");
}
}
}
return gitDir;
}
}
1、手動(dòng)觸發(fā)
public void reloadSqlXml() {
try {
File mapperXmlDir = gitConfigRepository.getRepositoryDir();
mapperHotDeployPlugin.reloadSqlXml(mapperXmlDir);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
package com.jason.dao;
import cn.hutool.core.bean.BeanUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileInputStream;
import java.util.*;
import java.util.stream.Collectors;
/**
* mapper.xml熱部署,最小單位是一個(gè)xml文件
*
* @author: jason
* @Date: 2022-01-13
*/
@Slf4j
@Component
public class MapperHotDeployPlugin implements InitializingBean {
@Autowired
private SqlSessionFactory sqlSessionFactory;
private volatile Configuration configuration;
@Override
public void afterPropertiesSet() {
configuration = sqlSessionFactory.getConfiguration();
}
public void reloadSqlXml(File file) {
if (file == null) {
return;
}
List<File> fileList = new ArrayList<>();
setFiles(file, fileList, ".xml");
reloadXml(fileList);
}
private void setFiles(File file, List<File> fileList, String suffix) {
File[] files = file.listFiles();
if (files == null || files.length == 0) {
return;
}
for (File f : files) {
if (f.isDirectory()) {
//遞歸調(diào)用
setFiles(f, fileList, suffix);
} else {
//保存文件路徑到集合中
if (f.getAbsolutePath().contains(suffix)) {
fileList.add(f);
}
}
}
}
/**
* 重新加載sql.xml
*
* @param fileList 修改的xml資源
*/
private void reloadXml(List<File> fileList) {
log.info("需要重新加載的文件列表: {}", fileList);
fileList.forEach(r -> {
try {
clearMap(getNamespace(r));
clearSet(r.getAbsolutePath());
XMLMapperBuilder xmlMapperBuilder =
new XMLMapperBuilder(new FileInputStream(r), getTarConfiguration(), r.toString(),
getTarConfiguration().getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
log.info("ERROR: 重新加載[{}]失敗", r.toString(), e);
throw new RuntimeException("ERROR: 重新加載[" + r.toString() + "]失敗", e);
} finally {
ErrorContext.instance().reset();
}
});
log.info("成功熱部署文件列表: {}", fileList);
}
private Configuration getTarConfiguration() {
return configuration;
}
/**
* 刪除xml元素的節(jié)點(diǎn)緩存
*
* @param nameSpace xml中命名空間
*/
private void clearMap(String nameSpace) {
log.info(
"清理Mybatis的namespace={}在mappedStatements、caches、resultMaps、parameterMaps、keyGenerators、sqlFragments中的緩存");
Arrays.asList("mappedStatements", "caches", "resultMaps", "parameterMaps", "keyGenerators", "sqlFragments")
.forEach(fieldName -> {
Object value = BeanUtil.getFieldValue(getTarConfiguration(), fieldName);
if (value instanceof Map) {
Map<?, ?> map = (Map) value;
List<Object> list = map.keySet().stream().filter(o -> o.toString().startsWith(nameSpace + "."))
.collect(Collectors.toList());
log.info("需要清理的元素: {}", list);
list.forEach(k -> map.remove((Object) k));
}
});
}
/**
* 清除文件記錄緩存
*
* @param resource xml文件路徑
*/
private void clearSet(String resource) {
log.info("清理mybatis的資源{}在容器中的緩存", resource);
Object value = BeanUtil.getFieldValue(getTarConfiguration(), "loadedResources");
if (value instanceof Set) {
Set<?> set = (Set) value;
set.remove(resource);
set.remove("namespace:" + resource);
}
}
/**
* 獲取xml的namespace
*
* @param file xml資源
* @return java.lang.String
*/
private String getNamespace(File file) {
try {
XPathParser parser = new XPathParser(new FileInputStream(file), true, null, new XMLMapperEntityResolver());
return parser.evalNode("/mapper").getStringAttribute("namespace");
} catch (Exception e) {
log.info("ERROR: 解析xml中namespace失敗", e);
throw new RuntimeException("ERROR: 解析xml中namespace失敗", e);
}
}
}
2、自動(dòng)監(jiān)控
package com.jason.replacer.config;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Collectors;
/**
* mapper.xml熱部署,最小單位是一個(gè)xml文件
*
* @author: jason
* @Date: 2022-01-13
*/
@Slf4j
@Component
public class MapperHotDeployPlugin implements InitializingBean {
@Autowired
private SqlSessionFactory sqlSessionFactory;
private volatile Configuration configuration;
@Value("${mybatis.mapper-locations}")
private String mybatisPath;
@Override
public void afterPropertiesSet() {
configuration = sqlSessionFactory.getConfiguration();
new WatchThread().start();
}
class WatchThread extends Thread {
@Override
public void run() {
startWatch();
}
/**
* 啟動(dòng)監(jiān)聽
*/
private void startWatch() {
try {
WatchService watcher = FileSystems.getDefault().newWatchService();
getWatchPaths().forEach(p -> {
try {
Paths.get(p).register(watcher, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
} catch (Exception e) {
log.error("ERROR: 注冊xml監(jiān)聽事件", e);
throw new RuntimeException("ERROR: 注冊xml監(jiān)聽事件", e);
}
});
while (true) {
WatchKey watchKey = watcher.take();
Set<String> set = new HashSet<>();
for (WatchEvent<?> event : watchKey.pollEvents()) {
set.add(event.context().toString());
}
// 重新加載xml
reloadXml(set);
boolean valid = watchKey.reset();
if (!valid) {
break;
}
}
} catch (Exception e) {
System.out.println("Mybatis的xml監(jiān)控失敗!");
log.info("Mybatis的xml監(jiān)控失敗!", e);
}
}
/**
* 加載需要監(jiān)控的文件父路徑
*
* @return java.util.Set<java.lang.String>
*/
private Set<String> getWatchPaths() {
Set<String> set = new HashSet<>();
Arrays.stream(getResource()).forEach(r -> {
try {
log.info("資源路徑:{}", r.toString());
set.add(r.getFile().getParentFile().getAbsolutePath());
} catch (Exception e) {
log.info("獲取資源路徑失敗", e);
throw new RuntimeException("獲取資源路徑失敗");
}
});
log.info("需要監(jiān)聽的xml資源: {}", set);
return set;
}
/**
* 獲取配置的mapperLocations
*
* @return org.springframework.core.io.Resource[]
*/
@SneakyThrows
private Resource[] getResource() {
return new PathMatchingResourcePatternResolver().getResources(mybatisPath);
}
/**
* 刪除xml元素的節(jié)點(diǎn)緩存
*
* @param nameSpace xml中命名空間
*/
private void clearMap(String nameSpace) {
log.info(
"清理Mybatis的namespace={}在mappedStatements、caches、resultMaps、parameterMaps、keyGenerators、sqlFragments中的緩存");
Arrays.asList("mappedStatements", "caches", "resultMaps", "parameterMaps", "keyGenerators", "sqlFragments")
.forEach(fieldName -> {
Object value = getFieldValue(configuration, fieldName);
if (value instanceof Map) {
Map<?, ?> map = (Map) value;
List<Object> list =
map.keySet().stream().filter(o -> o.toString().startsWith(nameSpace + "."))
.collect(Collectors.toList());
log.info("需要清理的元素: {}", list);
list.forEach(k -> map.remove((Object) k));
}
});
}
/**
* 清除文件記錄緩存
*
* @param resource xml文件路徑
*/
private void clearSet(String resource) {
log.info("清理mybatis的資源{}在容器中的緩存", resource);
Object value = getFieldValue(configuration, "loadedResources");
if (value instanceof Set) {
Set<?> set = (Set) value;
set.remove(resource);
set.remove("namespace:" + resource);
}
}
/**
* 獲取對象指定屬性
*
* @param obj 對象信息
* @param fieldName 屬性名稱
* @return java.lang.Object
*/
private Object getFieldValue(Object obj, String fieldName) {
log.info("從{}中加載{}屬性", obj, fieldName);
try {
Field field = obj.getClass().getDeclaredField(fieldName);
boolean accessible = field.isAccessible();
field.setAccessible(true);
Object value = field.get(obj);
field.setAccessible(accessible);
return value;
} catch (Exception e) {
log.info("ERROR: 加載對象中[{}]", fieldName, e);
throw new RuntimeException("ERROR: 加載對象中[" + fieldName + "]", e);
}
}
/**
* 重新加載set中xml
*
* @param set 修改的xml資源
*/
private void reloadXml(Set<String> set) {
log.info("需要重新加載的文件列表: {}", set);
List<Resource> list = Arrays.stream(getResource()).filter(p -> set.contains(p.getFilename()))
.collect(Collectors.toList());
log.info("需要處理的資源路徑:{}", list);
list.forEach(r -> {
try {
clearMap(getNamespace(r));
clearSet(r.toString());
XMLMapperBuilder xmlMapperBuilder =
new XMLMapperBuilder(r.getInputStream(), configuration, r.toString(),
configuration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
log.info("ERROR: 重新加載[{}]失敗", r.toString(), e);
throw new RuntimeException("ERROR: 重新加載[" + r.toString() + "]失敗", e);
} finally {
ErrorContext.instance().reset();
}
});
log.info("成功熱部署文件列表: {}", set);
}
/**
* 獲取xml的namespace
*
* @param resource xml資源
* @return java.lang.String
*/
private String getNamespace(Resource resource) {
log.info("從{}獲取namespace", resource.toString());
try {
XPathParser parser =
new XPathParser(resource.getInputStream(), true, null, new XMLMapperEntityResolver());
return parser.evalNode("/mapper").getStringAttribute("namespace");
} catch (Exception e) {
log.info("ERROR: 解析xml中namespace失敗", e);
throw new RuntimeException("ERROR: 解析xml中namespace失敗", e);
}
}
}
}
注
上面提供了加載mapper.xml文件的兩種方式
讀取文件絕對路徑
public static List<File> getFiles(String dirPath) {
List<File> fileList = new ArrayList<>();
File file = new File(dirPath);
return getFiles(file, fileList, ".xml");
}
public static List<File> getFiles(File file, List<File> fileList, String suffix) {
File[] files = file.listFiles();
if (files == null || files.length == 0) {
return Collections.emptyList();
}
for (File f : files) {
if (f.isDirectory()) {
//遞歸調(diào)用
getFiles(f, fileList, suffix);
} else {
//保存文件路徑到集合中
if (f.getAbsolutePath().contains(suffix)) {
fileList.add(f);
}
}
}
return fileList;
}
讀取classpath下的資源路徑
@SneakyThrows
private Resource[] getResource() {
return new PathMatchingResourcePatternResolver().getResources("classpath:mappers/**/*.xml");
}
總結(jié)
到此這篇關(guān)于Mybatis中mapper.xml實(shí)現(xiàn)熱加載介紹的文章就介紹到這了,更多相關(guān)Mybatis mapper.xml熱加載內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot項(xiàng)目中Controller接收兩個(gè)實(shí)體的實(shí)現(xiàn)方法
本文主要介紹了SpringBoot項(xiàng)目中Controller接收兩個(gè)實(shí)體的實(shí)現(xiàn)方法,主要介紹了兩種方法,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08
java.io.EOFException產(chǎn)生原因及解決方法(附代碼)
java.io.EOFException表示在讀取數(shù)據(jù)時(shí)突然遇到了文件或流的末尾,也就是說客戶端或服務(wù)器已經(jīng)關(guān)閉了連接,但是你還在嘗試讀取數(shù)據(jù),這篇文章主要給大家介紹了關(guān)于java.io.EOFException產(chǎn)生原因及解決的相關(guān)資料,需要的朋友可以參考下2023-09-09
Java的Hibernate框架中用于操作數(shù)據(jù)庫的HQL語句講解
這篇文章主要介紹了Java的Hibernate框架中用于操作數(shù)據(jù)庫的HQL語句講解,Hibernate是Java的SSH三大web開發(fā)框架之一,需要的朋友可以參考下2016-01-01
java工具類之實(shí)現(xiàn)java獲取文件行數(shù)
這篇文章主要介紹了一個(gè)java工具類,可以取得當(dāng)前項(xiàng)目中所有java文件總行數(shù),代碼行數(shù),注釋行數(shù),空白行數(shù),需要的朋友可以參考下2014-03-03
Spring的連接數(shù)據(jù)庫以及JDBC模板(實(shí)例講解)
下面小編就為大家?guī)硪黄猄pring的連接數(shù)據(jù)庫以及JDBC模板(實(shí)例講解)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10
dubbo將異常轉(zhuǎn)換成RuntimeException的原因分析?ExceptionFilter
這篇文章主要介紹了dubbo將異常轉(zhuǎn)換成RuntimeException的原因分析?ExceptionFilter問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
Java的RocketMq水平擴(kuò)展及負(fù)載均衡詳解
這篇文章主要介紹了Java的RocketMq水平擴(kuò)展及負(fù)載均衡詳解,RocketMQ是一個(gè)分布式具有高度可擴(kuò)展性的消息中間件,本文旨在探索在broker端,生產(chǎn)端,以及消費(fèi)端是如何做到橫向擴(kuò)展以及負(fù)載均衡的,需要的朋友可以參考下2024-01-01

