Mybatis mapper.xml热加载
程序猿小白菜 人气:0背景
有些需求可能更新sql的频率较高,但又不想频繁发布java应用程序,所以mybatis-mapper.xml热加载的需求顺势而出。
目的
只需调起加载mapper.xml的程序,无需重启整个java应用,低耦合。
实现方式
mapper.xml可以指定路径。如springboot工程resources目录下;亦可独立维护在某个git仓库,然后由程序加载到运行机器上去。
具体加载git仓库到运行机器代码如下:
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("无法删除临时文件: " + gitDir.getAbsolutePath()); } if (!gitDir.mkdir()) { throw new IOException("创建历史Git本地目录失败: " + gitDir.getAbsolutePath()); } gitDir.deleteOnExit(); isNewDir = true; localRepositoryTimestamp = now; } if (StringUtils.isNotEmpty(gitRepositoryURL)) { //设置远程服务器上的用户名和密码 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、手动触发
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热部署,最小单位是一个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()) { //递归调用 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元素的节点缓存 * * @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、自动监控
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热部署,最小单位是一个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(); } /** * 启动监听 */ 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监听事件", e); throw new RuntimeException("ERROR: 注册xml监听事件", 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监控失败!"); log.info("Mybatis的xml监控失败!", e); } } /** * 加载需要监控的文件父路径 * * @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("需要监听的xml资源: {}", set); return set; } /** * 获取配置的mapperLocations * * @return org.springframework.core.io.Resource[] */ @SneakyThrows private Resource[] getResource() { return new PathMatchingResourcePatternResolver().getResources(mybatisPath); } /** * 删除xml元素的节点缓存 * * @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()) { //递归调用 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"); }
总结
加载全部内容