最近参与了使用springboot+mybatis-plus的项目,使用jrebel能很好的解决sql变化以外的问题,但是在调式的时候,发现sql语句有任何的变化都需要重起项目才能生效。我们的项目框架原因,启动至少要花3分钟,一个稍复杂的sql可能来回修改几次,就得反反复复重起项目,大量的时候耗费在重起上。
尝试网上搜了一些资料,基本只有针对xml配置的sql有动态加载的资料,还比较复杂。而我们的项目,所有的sql都是用注解实现的,搜到一篇也是针对xml修改后加载的文章:https://blog.csdn.net/qq_24434671/article/details/90258908
此文没有解决我的问题,但是很有参参价值,在他的基础上做了改进,实现了我需要修改注解的sql后动态加载的功能。
此文给的方法存在的问题:
1,只能重新加载xml配置的sql。
2,自己定义的方法能动态加载,mapper超类中的通用方法动态加载后就消失了。
3,任何mapper的修改都加载所有的mapper。
经过改进后,使用更简单,只需要在启动类添加一个方法,指定Mapper类和xml路径即可:如图:
启动类中添加的内容为:(修改成自己项目mapper文件对应的路径即可)
@Bean
public MapperFileLoader mapperFileLoader() {
return new MapperFileLoader(new String[]{
"classpath:mapper/**/*.xml",
"classpath:com/dzw/mapper/*Mapper.class"
});
}
以下为mybatis-plus下的完整代码,tk.bybatis需要注释掉removeConfig方法。其它的请使用后反馈问题。
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.ibatis.builder.annotation.MapperAnnotationBuilder;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
/*
* dongzhiwei
* 实现mapper修改后动态加载功能
* */
public class MapperFileLoader {
private Logger logger = LoggerFactory.getLogger(MapperFileLoader.class);
@Autowired
private SqlSessionFactory sqlSessionFactory;
private static Configuration configuration=null;
private static Map<String,Class<?>> classMap = new HashMap<>();
private static Set<String> delMethodSet = new HashSet<>();
private static final int interValCheckMin = 5;
private static int checkNum=0;
private static boolean stop = false;
private Resource[] mapperLocations;
private String[] packageSearchPath = {"classpath:mapper/**/*.xml","classpath:**/*Mapper.class"};
private HashMap<String, Long> fileMapping = new HashMap<String, Long>();
public MapperFileLoader(String[] packageSearchPath) {
if(packageSearchPath != null && packageSearchPath.length>0) {
this.packageSearchPath = packageSearchPath;
}
startThreadListener();
}
public void startThreadListener() {
ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if(stop){
service.shutdown();
service.shutdownNow();
}
checkNum++;
try{
checkFileIsChanged();
}catch(Exception e){
e.printStackTrace();
}
}
}, 3, interValCheckMin,TimeUnit.SECONDS );
}
public String reloadMapperFile(Set<String> changeFileSet) {
if(configuration==null){
configuration = sqlSessionFactory.getConfiguration();
}
try {
//清理 tk.mybatis下需要注释掉
this.removeConfig(configuration);
//重新加载
for (Resource configLocation : mapperLocations) {
if(changeFileSet.size()<=0){
break;
}
try {
String resName = configLocation.toString().replace("[", "").replace("]", "");
resName = resName.substring(resName.lastIndexOf("\\")+1);
if(!changeFileSet.contains(resName)){//没变化的项不需要重新加载
continue;
}
changeFileSet.remove(resName);
if(resName.endsWith(".xml")){
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configLocation.getInputStream(), configuration, configLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();
}else if(resName.endsWith(".class")){
String className = configLocation.toString();
className = className.substring(className.indexOf("\\classes\\")+"\\classes\\".length(),className.length()-".class".length()-1).replace("\\", ".");
Class<?> clazz = null;
if(classMap.containsKey(className)){
clazz = classMap.get(className);
}else{
clazz = Class.forName(className);
classMap.put(className, clazz);
}
for(Method m:clazz.getDeclaredMethods()){
delMethodSet.add(clazz.getName()+"."+m.getName());
}
if(delMethodSet.size()>0){
clearMap(configuration.getClass(), configuration, "mappedStatements");
MapperAnnotationBuilder mapperAnnotationBuilder = new MapperAnnotationBuilder(sqlSessionFactory.getConfiguration(),clazz);
mapperAnnotationBuilder.parse();
}
}
logger.info("mapper文件[" + configLocation.getFilename() + "]重新加载成功");
} catch (Exception e) {
logger.error("mapper文件[" + configLocation.getFilename() + "]不存在或内容格式不对");
continue;
}
}
return "刷新mybatis xml配置语句成功";
} catch (Exception e) {
e.printStackTrace();
return "刷新mybatis xml配置语句失败";
}
}
public void setPackageSearchPath(String[] packageSearchPath) {
this.packageSearchPath = packageSearchPath;
}
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
/**
* 扫描xml文件所在的路径
* @throws IOException
*/
private void scanMapper() throws IOException {
List<Resource> listRes = new ArrayList<>(32);
for(String path:packageSearchPath){
try{
Resource[] tmp = new PathMatchingResourcePatternResolver().getResources(path);
listRes.addAll(Arrays.asList(tmp));
}catch(Exception e){
logger.error("扫描mapper文件出错:"+(e.getMessage()!=null?e.getMessage():e.getCause()));
}
}
if(listRes==null || listRes.size()<0){
logger.error("mapper资源文件为空!退出任务");
stop=true;
}
this.mapperLocations = new Resource[listRes.size()];
for(int i=0;i<listRes.size();i++){
this.mapperLocations[i]=listRes.get(i);
}
}
/**
* 清空Configuration中几个重要的缓存
* @param configuration
* @throws Exception
*/
private void removeConfig(Configuration configuration) throws Exception {
Class<?> classConfig = configuration.getClass();
clearMap(classConfig, configuration, "mappedStatements");//不能直接刷新此项,否则父类的方法就不能再用了
clearMap(classConfig, configuration, "caches");
clearMap(classConfig, configuration, "resultMaps");
clearMap(classConfig, configuration, "parameterMaps");
clearMap(classConfig, configuration, "keyGenerators");
clearMap(classConfig, configuration, "sqlFragments");
clearSet(classConfig, configuration, "loadedResources");
}
@SuppressWarnings("rawtypes")
private void clearMap(Class<?> classConfig, Configuration configuration, String fieldName) throws Exception {
Field field = null;
if(configuration.getClass().getName().equals("com.baomidou.mybatisplus.core.MybatisConfiguration")) {
field = classConfig.getSuperclass().getDeclaredField(fieldName);
}else {
field = classConfig.getClass().getDeclaredField(fieldName);
}
field.setAccessible(true);
Map mapConfig = (Map) field.get(configuration);
if(fieldName.equals("mappedStatements")){
for(String delKey:delMethodSet){
if(mapConfig.containsKey(delKey)){
mapConfig.remove(delKey);
}
}
delMethodSet.clear();
return;
}
mapConfig.clear();
}
@SuppressWarnings("rawtypes")
private void clearSet(Class<?> classConfig, Configuration configuration, String fieldName) throws Exception {
Field field = null;
if(configuration.getClass().getName().equals("com.baomidou.mybatisplus.core.MybatisConfiguration")) {
field = classConfig.getSuperclass().getDeclaredField(fieldName);
}else {
field = classConfig.getClass().getDeclaredField(fieldName);
}
field.setAccessible(true);
Set setConfig = (Set) field.get(configuration);
setConfig.clear();
}
/**
* 判断文件是否发生了变化并动态加载
* @param resource
* @return
* @throws IOException
*/
public void checkFileIsChanged() {
try {
this.scanMapper();
} catch (IOException e) {
e.printStackTrace();
}
Set<String> changeFileSet = new HashSet<>();
for (Resource resource : mapperLocations) {
String resourceName = resource.getFilename();
boolean addFlag = !fileMapping.containsKey(resourceName);
// 修改文件:判断文件内容是否有变化
Long compareFrame = fileMapping.get(resourceName);
long lastData=0;
try {
lastData = resource.contentLength() + resource.lastModified();
} catch (IOException e) {
e.printStackTrace();
}
boolean modifyFlag = null != compareFrame && compareFrame.longValue() != lastData;
// 新增或是修改时,重新加载文件
if(addFlag || modifyFlag) {
fileMapping.put(resourceName, Long.valueOf(lastData));
if (checkNum > 1) {
changeFileSet.add(resourceName);
}
}
}
if(changeFileSet.size()>0){
reloadMapperFile(changeFileSet);
}
}
}