Mybatis中mapper.xml實(shí)現(xiàn)熱加載介紹
背景
有些需求可能更新sql的頻率較高,但又不想頻繁發(fā)布java應(yīng)用程序,所以mybatis-mapper.xml熱加載的需求順勢(shì)而出。
目的
只需調(diào)起加載mapper.xml的程序,無(wú)需重啟整個(gè)java應(yīng)用,低耦合。
實(shí)現(xiàn)方式
mapper.xml可以指定路徑。如springboot工程resources目錄下;亦可獨(dú)立維護(hù)在某個(gè)git倉(cāng)庫(kù),然后由程序加載到運(yùn)行機(jī)器上去。
具體加載git倉(cāng)庫(kù)到運(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("無(wú)法刪除臨時(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) { //克隆代碼庫(kù)命令 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)聽(tīng) */ 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: 注冊(cè)xml監(jiān)聽(tīng)事件", e); throw new RuntimeException("ERROR: 注冊(cè)xml監(jiān)聽(tīng)事件", 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)聽(tīng)的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); } } /** * 獲取對(duì)象指定屬性 * * @param obj 對(duì)象信息 * @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: 加載對(duì)象中[{}]", fieldName, e); throw new RuntimeException("ERROR: 加載對(duì)象中[" + 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文件的兩種方式
讀取文件絕對(duì)路徑
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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- MyBatis源碼剖析之Mapper代理方式詳解
- mybatis配置mapper-locations的坑及解決
- mybatis中mapper-locations的作用
- Mybatis的mapper.xml中if標(biāo)簽test判斷的用法說(shuō)明
- MyBatis實(shí)現(xiàn)注冊(cè)及獲取Mapper
- mybatis中的mapper.xml使用循環(huán)語(yǔ)句
- 關(guān)于MyBatis中Mapper?XML熱加載優(yōu)化
- mybatis?mapper.xml中如何根據(jù)數(shù)據(jù)庫(kù)類型選擇對(duì)應(yīng)SQL語(yǔ)句
- MyBatis映射器mapper快速入門(mén)教程
相關(guān)文章
SpringBoot項(xiàng)目中Controller接收兩個(gè)實(shí)體的實(shí)現(xiàn)方法
本文主要介紹了SpringBoot項(xiàng)目中Controller接收兩個(gè)實(shí)體的實(shí)現(xiàn)方法,主要介紹了兩種方法,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08java.io.EOFException產(chǎn)生原因及解決方法(附代碼)
java.io.EOFException表示在讀取數(shù)據(jù)時(shí)突然遇到了文件或流的末尾,也就是說(shuō)客戶端或服務(wù)器已經(jīng)關(guān)閉了連接,但是你還在嘗試讀取數(shù)據(jù),這篇文章主要給大家介紹了關(guān)于java.io.EOFException產(chǎn)生原因及解決的相關(guān)資料,需要的朋友可以參考下2023-09-09Java的Hibernate框架中用于操作數(shù)據(jù)庫(kù)的HQL語(yǔ)句講解
這篇文章主要介紹了Java的Hibernate框架中用于操作數(shù)據(jù)庫(kù)的HQL語(yǔ)句講解,Hibernate是Java的SSH三大web開(kāi)發(fā)框架之一,需要的朋友可以參考下2016-01-01java工具類之實(shí)現(xiàn)java獲取文件行數(shù)
這篇文章主要介紹了一個(gè)java工具類,可以取得當(dāng)前項(xiàng)目中所有java文件總行數(shù),代碼行數(shù),注釋行數(shù),空白行數(shù),需要的朋友可以參考下2014-03-03Spring的連接數(shù)據(jù)庫(kù)以及JDBC模板(實(shí)例講解)
下面小編就為大家?guī)?lái)一篇Spring的連接數(shù)據(jù)庫(kù)以及JDBC模板(實(shí)例講解)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10Java?awt-對(duì)話框簡(jiǎn)單實(shí)現(xiàn)方式
這篇文章主要介紹了Java?awt-對(duì)話框簡(jiǎn)單實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12dubbo將異常轉(zhuǎn)換成RuntimeException的原因分析?ExceptionFilter
這篇文章主要介紹了dubbo將異常轉(zhuǎn)換成RuntimeException的原因分析?ExceptionFilter問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03Java的RocketMq水平擴(kuò)展及負(fù)載均衡詳解
這篇文章主要介紹了Java的RocketMq水平擴(kuò)展及負(fù)載均衡詳解,RocketMQ是一個(gè)分布式具有高度可擴(kuò)展性的消息中間件,本文旨在探索在broker端,生產(chǎn)端,以及消費(fèi)端是如何做到橫向擴(kuò)展以及負(fù)載均衡的,需要的朋友可以參考下2024-01-01