需求:
项目在开发阶段或是修复bug阶段,会有修改mybatis的mapper. 的时候,修改一般情况都要重启才能生失效,如果是分布式项目重启有时会耗时很久,都是无尽的等待。如果频繁修改,那么时间都浪费到等待重启的过程。
目标:
实现mybatis的mapper. 文件修改后热部署,而且只热更新修改了的 ,可以提高重新解析过程的效率。
要求:
尽量满足开闭原则
实现:
import com.yirun. work.core.utils.PropertiesHolder;
import org.apache.commons.lang.StringUtils;
import org.apache.ibatis.builder. . MapperBuilder;
import org.apache.ibatis.builder. . MapperEntityResolver;
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.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spring work.beans.BeansException;
import org.spring work.beans.factory.InitializingBean;
import org.spring work.context.ApplicationContext;
import org.spring work.context.ApplicationContextAware;
import org.spring work.core.io.Resource;
import java.lang.reflect.Field;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Collectors;
/**
* mapper. 热部署,最小单位是一个 文件
* @date :2018/12/20
* @author :zc.ding@foxmail.com
*/
public class MapperHotDeployPlugin implements InitializingBean, ApplicationContextAware {
private final static Logger logger = LoggerFactory.getLogger(MapperHotDeployPlugin.class);
private final static String OPEN = \"1\";
private volatile SqlSessionFactoryBean sqlSessionFactoryBean;
private volatile Configuration configuration;
@Override
public void afterPropertiesSet() {
String flag = PropertiesHolder.getProperty(\"mapper.hot.deploy\");
logger.info(\"Mybatis热部署标识mapper.hot.deploy={}\", flag);
// 判断是否开启了热部署
if(StringUtils.isNotBlank(flag) && OPEN.equals(flag)){
new WatchThread().start();
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) applicationContext.getBean(\" workSqlSessionFactory\");
sqlSessionFactoryBean = applicationContext.getBean(SqlSessionFactoryBean.class);
configuration = sqlSessionFactory.getConfiguration();
}
class WatchThread extends Thread{
private final Logger logger = LoggerFactory.getLogger(WatchThread.class);
@Override
public void run() {
startWatch();
}
/**
* 启动监听
* @date :2018/12/19
* @author :zc.ding@foxmail.com
*/
private void startWatch(){
try{
WatchService watcher = FileSystems.getDefault().newWatchService();
getWatchPaths().forEach(p -> {
try {
Paths.get(p).register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
} catch (Exception e) {
logger.error(\"ERROR: 注册 监听事件\", e);
throw new RuntimeException(\"ERROR: 注册 监听事件\", e);
}
});
while (true) {
WatchKey watchKey = watcher.take();
Set<String> set = new HashSet<>();
for (WatchEvent<?> event: watchKey.pollEvents()) {
set.add(event.context().toString());
}
// 重新加载
reload (set);
boolean valid = watchKey.reset();
if (!valid) {
break;
}
}
}catch(Exception e){
System.out.println(\"Mybatis的 监控失败!\");
logger.info(\"Mybatis的 监控失败!\", e);
}
}
/**
* 加载需要监控的文件父路径
* @return java.util.Set<java.lang.String>
* @date :2018/12/19
* @author :zc.ding@foxmail.com
*/
private Set<String> getWatchPaths(){
Set<String> set = new HashSet<>();
Arrays.stream(getResource()).forEach(r -> {
try{
logger.info(\"资源路径:{}\", r.toString());
set.add(r.getFile().getParentFile().getAbsolutePath());
}catch(Exception e){
logger.info(\"获取资源路径失败\", e);
throw new RuntimeException(\"获取资源路径失败\");
}
});
logger.info(\"需要监听的 资源: {}\", set);
return set;
}
/**
* 获取配置的mapperLocations
* @return org.spring work.core.io.Resource[]
* @date :2018/12/19
* @author :zc.ding@foxmail.com
*/
private Resource[] getResource(){
return (Resource[]) getFieldValue(sqlSessionFactoryBean, \"mapperLocations\");
}
/**
* 删除 元素的节点缓存
* @param nameSpace 中命名空间
* @date :2018/12/19
* @author :zc.ding@foxmail.com
*/
private void clearMap(String nameSpace) {
logger.info(\"清理Mybatis的namespace={}在mappedStatements、caches、resultMaps、parameterMaps、keyGenerators、sqlFragments中的缓存\");
Arrays.asList(\"mappedStatements\", \"caches\", \"resultMaps\", \"parameterMaps\", \"keyGenerators\", \"sqlFragments\").forEach(fieldName -> {
value = getFieldValue(configuration, fieldName);
if (value instanceof Map) {
Map<?, ?> map = (Map)value;
List< > list = map.keySet().stream().filter(o -> o.toString().startsWith(nameSpace + \".\")).collect(Collectors.toList());
logger.info(\"需要清理的元素: {}\", list);
list.forEach(k -> map.remove(( )k));
}
});
}
/**
* 清除文件记录缓存
* @param resource 文件路径
* @date :2018/12/19
* @author :zc.ding@foxmail.com
*/
private void clearSet(String resource) {
logger.info(\"清理mybatis的资源{}在容器中的缓存\", resource);
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.
* @date :2018/12/19
* @author :zc.ding@foxmail.com
*/
private getFieldValue( obj, String fieldName){
logger.info(\"从{}中加载{}属性\", obj, fieldName);
try{
Field field = obj.getClass().getDeclaredField(fieldName);
boolean accessible = field.isAccessible();
field.setAccessible(true);
value = field.get(obj);
field.setAccessible(accessible);
return value;
}catch(Exception e){
logger.info(\"ERROR: 加载对象中[{}]\", fieldName, e);
throw new RuntimeException(\"ERROR: 加载对象中[\" + fieldName + \"]\", e);
}
}
/**
* 重新加载set中
* @param set 修改的 资源
* @date :2018/12/19
* @author :zc.ding@foxmail.com
*/
private void reload (Set<String> set){
logger.info(\"需要重新加载的文件列表: {}\", set);
List<Resource> list = Arrays.stream(getResource())
.filter(p -> set.contains(p.getFilename()))
.collect(Collectors.toList());
logger.info(\"需要处理的资源路径:{}\", list);
list.forEach(r ->{
try{
clearMap(getNamespace(r));
clearSet(r.toString());
MapperBuilder MapperBuilder = new MapperBuilder(r.getInputStream(), configuration,
r.toString(), configuration.getSqlFragments());
MapperBuilder.parse();
}catch(Exception e){
logger.info(\"ERROR: 重新加载[{}]失败\", r.toString(), e);
throw new RuntimeException(\"ERROR: 重新加载[\" + r.toString() + \"]失败\", e);
}finally {
ErrorContext.instance().reset();
}
});
logger.info(\"成功热部署文件列表: {}\", set);
}
/**
* 获取 的namespace
* @param resource 资源
* @return java.lang.String
* @date :2018/12/19
* @author :zc.ding@foxmail.com
*/
private String getNamespace(Resource resource){
logger.info(\"从{}获取namespace\", resource.toString());
try{
XPathParser parser = new XPathParser(resource.getInputStream(), true, null, new MapperEntityResolver());
return parser.evalNode(\"/mapper\").getStringAttribute(\"namespace\");
}catch(Exception e){
logger.info(\"ERROR: 解析 中namespace失败\", e);
throw new RuntimeException(\"ERROR: 解析 中namespace失败\", e);
}
}
}
}
待优化:
支持单数据源,使用开发环境
good luck!
继续阅读与本文标签相同的文章
下一篇 :
物流科技企业如何做运力金融?如何得到银行的信任?
-
【Java类初始化死锁】记一次Cassandra死锁问题排查
2026-05-18栏目: 教程
-
搭建自己的技术博客系列(一)使用 hexo 搭建一个精美的静态博客
2026-05-18栏目: 教程
-
搭建自己的技术博客系列(二)把 Hexo 博客部署到 GitHub 上
2026-05-18栏目: 教程
-
CMU 15-721 14-数据库调度 Scheduling
2026-05-18栏目: 教程
-
从校招生到核心架构师,支付宝研究员李俊奎谈如何成为一名优秀的程序员
2026-05-18栏目: 教程
