本文适用修改JAVA代码热部署、MyBatis XML的热部署。
一、JAVA代码热部署.
新版IDEA中:开启允许在运行过程中修改文件
最后要在Debug模式启动,可以看到热部署的加载文件了,可以手动点左边那个图标立即加载生效.
二、MyBatis XML修改热部署.
MybatisMapperRefresh (mybatis-plus 2.0.7 API)
MyBatis在2.x的版本中移除了热加载的插件,我们实现的思路是手动加入这个MyBatis刷新的线程,本文以微服务框架SpringBoot为例.
引入依赖,注意和SpringBoot的版本适配。
<!-- mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
MybatisMapperRefresh
package com.boot.skywalk.config;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
import com.baomidou.mybatisplus.core.toolkit.SystemClock;
import com.google.common.collect.Lists;
import org.apache.ibatis.binding.MapperRegistry;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.util.ResourceUtils;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MybatisMapperRefresh implements Runnable{
private static final Log logger = LogFactory.getLog(MybatisMapperRefresh.class);
/**
* 记录jar包存在的mapper
*/
private static final Map<String, List<Resource>> jarMapper = new HashMap<>();
private SqlSessionFactory sqlSessionFactory;
private Resource[] mapperLocations;
private volatile Long beforeTime = 0L;
private Configuration configuration;
/**
* 是否开启刷新mapper
*/
private boolean enabled;
/**
* xml文件目录
*/
private Set<String> fileSet;
/**
* 延迟加载时间
*/
private int delaySeconds = 10;
/**
* 刷新间隔时间
*/
private int sleepSeconds = 20;
public MybatisMapperRefresh(Resource[] mapperLocations, SqlSessionFactory sqlSessionFactory, int delaySeconds,
int sleepSeconds, boolean enabled) {
this.mapperLocations = mapperLocations.clone();
this.sqlSessionFactory = sqlSessionFactory;
this.delaySeconds = delaySeconds;
this.enabled = enabled;
this.sleepSeconds = sleepSeconds;
this.configuration = sqlSessionFactory.getConfiguration();
this.run();
}
public MybatisMapperRefresh(Resource[] mapperLocations, SqlSessionFactory sqlSessionFactory, boolean enabled) {
this.mapperLocations = mapperLocations.clone();
this.sqlSessionFactory = sqlSessionFactory;
this.enabled = enabled;
this.configuration = sqlSessionFactory.getConfiguration();
this.run();
}
@Override
public void run() {
final GlobalConfig globalConfig = GlobalConfigUtils.getGlobalConfig(configuration);
/*
* 启动 XML 热加载
*/
if (enabled) {
beforeTime = SystemClock.now();
final MybatisMapperRefresh runnable = this;
new Thread(new Runnable() {
@Override
public void run() {
if (fileSet == null) {
fileSet = new HashSet<>();
if (mapperLocations != null) {
for (Resource mapperLocation : mapperLocations) {
try {
if (ResourceUtils.isJarURL(mapperLocation.getURL())) {
String key = new UrlResource(
ResourceUtils.extractJarFileURL(mapperLocation.getURL())).getFile()
.getPath();
fileSet.add(key);
if (jarMapper.get(key) != null) {
jarMapper.get(key).add(mapperLocation);
} else {
List<Resource> resourcesList = new ArrayList<>();
resourcesList.add(mapperLocation);
jarMapper.put(key, resourcesList);
}
} else {
fileSet.add(mapperLocation.getFile().getPath());
}
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
}
try {
Thread.sleep(delaySeconds * 1000);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
do {
try {
for (String filePath : fileSet) {
File file = new File(filePath);
if (file.isFile() && file.lastModified() > beforeTime) {
// 记录上次重新加载时间防止重复加载已经重载的文件
beforeTime = file.lastModified();
List<Resource> removeList = jarMapper.get(filePath);
if (removeList != null && !removeList.isEmpty()) {
for (Resource resource : removeList) {
runnable.refresh(resource);
}
} else {
runnable.refresh(new FileSystemResource(file));
}
}
}
} catch (Exception exception) {
exception.printStackTrace();
}
try {
Thread.sleep(sleepSeconds * 1000);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
} while (true);
}
}, "mybatis-plus MapperRefresh").start();
}
}
/**
* 刷新mapper
*
* @throws Exception
*/
@SuppressWarnings("rawtypes")
private void refresh(Resource resource)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
this.configuration = sqlSessionFactory.getConfiguration();
boolean isSupper = configuration.getClass().getSuperclass() == Configuration.class;
try {
Field loadedResourcesField = isSupper
? configuration.getClass().getSuperclass().getDeclaredField("loadedResources")
: configuration.getClass().getDeclaredField("loadedResources");
loadedResourcesField.setAccessible(true);
Set loadedResourcesSet = ((Set) loadedResourcesField.get(configuration));
XPathParser xPathParser = new XPathParser(resource.getInputStream(), true, configuration.getVariables(),
new XMLMapperEntityResolver());
XNode context = xPathParser.evalNode("/mapper");
String namespace = context.getStringAttribute("namespace");
Field field = MapperRegistry.class.getDeclaredField("knownMappers");
field.setAccessible(true);
Map mapConfig = (Map) field.get(configuration.getMapperRegistry());
Collection<String> mappedStatementNames = configuration.getMappedStatementNames();
mapConfig.remove(Resources.classForName(namespace));
loadedResourcesSet.remove(resource.toString());
configuration.getCacheNames().remove(namespace);
cleanParameterMap(context.evalNodes("/mapper/parameterMap"), namespace);
cleanResultMap(context.evalNodes("/mapper/resultMap"), namespace);
cleanKeyGenerators(context.evalNodes("insert|update|select|delete"), namespace);
cleanSqlElement(context.evalNodes("/mapper/sql"), namespace);
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(resource.getInputStream(),
sqlSessionFactory.getConfiguration(), resource.toString(),
sqlSessionFactory.getConfiguration().getSqlFragments());
xmlMapperBuilder.parse();
logger.debug("refresh: '" + resource + "', success!");
} catch (IOException e) {
logger.error("Refresh IOException :" + e.getMessage());
} finally {
ErrorContext.instance().reset();
}
}
/**
* 清理parameterMap
*
* @param list
* @param namespace
*/
private void cleanParameterMap(List<XNode> list, String namespace) {
for (XNode parameterMapNode : list) {
String id = parameterMapNode.getStringAttribute("id");
configuration.getParameterMaps().remove(namespace + "." + id);
}
}
/**
* 清理resultMap
*
* @param list
* @param namespace
*/
private void cleanResultMap(List<XNode> list, String namespace) {
for (XNode resultMapNode : list) {
String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
configuration.getResultMapNames().remove(id);
configuration.getResultMapNames().remove(namespace + "." + id);
clearResultMap(resultMapNode, namespace);
}
}
private void clearResultMap(XNode xNode, String namespace) {
for (XNode resultChild : xNode.getChildren()) {
if ("association".equals(resultChild.getName()) || "collection".equals(resultChild.getName())
|| "case".equals(resultChild.getName())) {
if (resultChild.getStringAttribute("select") == null) {
configuration.getResultMapNames()
.remove(resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
configuration.getResultMapNames().remove(namespace + "."
+ resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
if (resultChild.getChildren() != null && !resultChild.getChildren().isEmpty()) {
clearResultMap(resultChild, namespace);
}
}
}
}
}
/**
* 清理selectKey
*
* @param list
* @param namespace
*/
private void cleanKeyGenerators(List<XNode> list, String namespace) {
for (XNode context : list) {
String id = context.getStringAttribute("id");
configuration.getKeyGeneratorNames().remove(id + SelectKeyGenerator.SELECT_KEY_SUFFIX);
configuration.getKeyGeneratorNames().remove(namespace + "." + id + SelectKeyGenerator.SELECT_KEY_SUFFIX);
Collection<MappedStatement> mappedStatements = configuration.getMappedStatements();
List<MappedStatement> objects = Lists.newArrayList();
Iterator<MappedStatement> it = mappedStatements.iterator();
while (it.hasNext()) {
Object object = it.next();
if (object instanceof org.apache.ibatis.mapping.MappedStatement) {
MappedStatement mappedStatement = (MappedStatement) object;
if (mappedStatement.getId().equals(namespace + "." + id)) {
objects.add(mappedStatement);
}
}
}
mappedStatements.removeAll(objects);
}
}
/**
* 清理sql节点缓存
*
* @param list
* @param namespace
*/
private void cleanSqlElement(List<XNode> list, String namespace) {
for (XNode context : list) {
String id = context.getStringAttribute("id");
configuration.getSqlFragments().remove(id);
configuration.getSqlFragments().remove(namespace + "." + id);
}
}
}
MybatisPlusConfig:在这里面修改多少秒加载参数和开关可配置化,注意生产环境严禁使用!!!
package com.boot.skywalk.config;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.apache.commons.collections4.IteratorUtils;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
@Autowired
private MybatisPlusProperties mybatisPlusProperties;
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
@Bean
public MybatisMapperRefresh mybatisMapperRefresh(ApplicationContext applicationContext,
SqlSessionFactory sqlSessionFactory) {
LinkedHashSet<Resource> mapperLocations = new LinkedHashSet<>();
for (String xx : mybatisPlusProperties.getMapperLocations()) {
try {
mapperLocations.addAll(Arrays.asList(applicationContext.getResources(xx)));
} catch (Exception e) {
continue;
}
}
List<Resource> list = IteratorUtils.toList(mapperLocations.iterator());
Resource[] array = list.toArray(new Resource[list.size()]);
MybatisMapperRefresh mybatisMapperRefresh = new MybatisMapperRefresh(array, sqlSessionFactory, 10, 5, true);
return mybatisMapperRefresh;
}
}
验证:修改Mapper中的SQL,过配置的时间就生效了。大型项目多模块开发环境比较实用.