Mybatis中mapper.xml实现热加载介绍

目录
  • 背景
  • 目的
  • 实现方式
  • 总结

背景

有些需求可能更新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");
		}

总结

到此这篇关于Mybatis中mapper.xml实现热加载介绍的文章就介绍到这了,更多相关Mybatis mapper.xml热加载内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 关于MyBatis中Mapper XML热加载优化

    前几天在琢磨mybatis xml热加载的问题,原理还是通过定时扫描xml文件去跟新,但放到项目上就各种问题,由于用了mybatisplus死活不生效.本着"即插即用"的原则,狠心把其中的代码优化了一遍,能够兼容mybatisplus,还加入了一些日志,直接上代码 package com.bzd.core.mybatis; import java.io.File; import java.io.FileNotFoundException; import java.io.IOExcept

  • Mybatis中mapper.xml实现热加载介绍

    目录 背景 目的 实现方式 总结 背景 有些需求可能更新sql的频率较高,但又不想频繁发布java应用程序,所以mybatis-mapper.xml热加载的需求顺势而出. 目的 只需调起加载mapper.xml的程序,无需重启整个java应用,低耦合. 实现方式 mapper.xml可以指定路径.如springboot工程resources目录下:亦可独立维护在某个git仓库,然后由程序加载到运行机器上去.具体加载git仓库到运行机器代码如下: package com.jason.git; im

  • mybatis xml文件热加载实现示例详解

    目录 引言 一.xml 文件热加载实现原理 1.1 xml 文件怎么样解析 1.2 实现思路 二.mybatis-xmlreload-spring-boot-starter 登场 2.1 核心代码 2.2 安装方式 2.3 使用配置 最后 引言 本文博主给大家带来一篇 mybatis xml 文件热加载的实现教程,自博主从事开发工作使用 Mybatis 以来,如果需要修改 xml 文件的内容,通常都需要重启项目,因为不重启的话,修改是不生效的,Mybatis 仅仅会在项目初始化的时候将 xml

  • 解决Mybatis中mapper.xml文件update,delete及insert返回值问题

    最近写了几个非常简单的接口(CRUD),在单元测试的时候却出了问题,报错如下: Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'messageListener': Unsatisfied dependency expressed through field 'reviewCheckInfoService'; nested exce

  • mybatis中mapper.xml文件的常用属性及标签讲解

    目录 ${}和#{}的区别 常见的属性 常见标签 < sql >标签 < where >和< if >标签 < set >标签 < trim>标签 < choose >标签 mybatis 的xml文件中标签错误 ${}和#{}的区别 #{}会自动在你要插入字段两端 加上引号.例如:你写的是order by #{username},传的是 zhangsan,那么会解析成order by "zhangsan". ${

  • MyBatis中Mapper的注入问题详解

    在 SpringBoot 体系中,MyBatis 对 Mapper 的注入常见的方式我知道的有 2 种: 1.@MapperScan MapperScan 类是 mybatis-spring 包里面的. 通过在启动类上使用 @MapperScan,然后通过 basePackages 属性指定 Mapper 文件所在的目录来进行扫描装载,默认情况下指定目录下的所有.java文件都会被当做 Mapper 来加载处理. @MapperScan(basePackages = "com.test.spri

  • mybatis的mapper.xml中resultMap标签的使用详解

    1.前言 最近博主在做一个ssm框架的共享汽车管理系统,其中,数据库字段设计的有下划线方式,a_username,然后在写mapper.xml里面的sql语句的时候,一直出现查询语句查询的值为null的情况.或者是resultMap标签和驼峰规则不太明白的同学,可以看这里. 于是顺便梳理一下. 2.关于resultMap 2.1.什么是resultMap? 在mybatis中有一个resultMap标签,它是为了映射select查询出来结果的集合,其主要作用是将实体类中的字段与数据库表中的字段进

  • web.xml中如何设置配置文件的加载路径实例详解

    web.xml中如何设置配置文件的加载路径实例详解 web应用程序通过Tomcat等容器启动时,会首先加载web.xml文件,通常我们工程中的各种配置文件,如日志.数据库.spring的文件等都在此时被加载,下面是两种常用的配置文件加载路径,即配置文件可以放到 SRC目录下或者可以放到WEB-INF根目录下 第一种在web.xml中这样配置: <context-param> <param-name >contextConfigLocation </param-name >

  • JS中使用gulp实现压缩文件及浏览器热加载功能

    gulp类似于grunt,都是基于Node.js的前端构建工具.不过gulp压缩效率更高. 一.安装gulp 首先,你要安装过nodejs,如果没有安装过的同学请自行下载.  先再命令行里输入   npm install gulp -g   下载gulp 二.创建gulp项目 创建一个你需要项目文件夹,然后在根目录输入  npm init  (npm init命令会为你创建一个package.json文件,这个文件保存着这个项目相关信息.比如你用到的各种依赖) 三.使用npm install 安

  • web.xml中servlet, bean, filter, listenr 加载顺序_动力节点Java学院整理

    web.xml 文件中一般包括 servlet, spring, filter, listenr的配置.那么他们是按照一个什么顺序加载呢?加载顺序会影响对spring bean 的调用. 比如filter需要用到 bean ,但是加载顺序是 先加载filter 后加载spring,则filter中初始化操作中的bean为null:首先可以肯定 加载顺序与他们在web.xml 文件中的先后顺序无关. web.xml 中 listener 和 serverlet 的加载顺序为 先 listener

随机推荐