SQLSessionCacheController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
  
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

@RestController
@RequestMapping("/ws/sql")
@Slf4j
public class SQLSessionCacheController {

@Autowired
private SqlSessionFactory sqlSessionFactory;

@Autowired
private SqlSessionFactoryBean sqlSessionFactoryBean;
private Resource[] mapperLocations;
private HashMap<String, Long> fileMapping = new HashMap<String, Long>();// 记录文件是否变化


@RequestMapping("/refresh")
@ResponseBody
public String refreshMapper() {
try {
Configuration configuration = sqlSessionFactory.getConfiguration();
// step.1 扫描文件
try {
scanMapperXml();
} catch (IOException e) {
log.error("packageSearchPath扫描包路径配置错误");
return "packageSearchPath扫描包路径配置错误";
}
// step.2 判断是否有文件发生了变化
if (isChanged()) {
// step.2.1 清理
removeConfig(configuration);
// step.2.2 重新加载
for (Resource configLocation : mapperLocations) {
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configLocation.getInputStream(), configuration, configLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();
log.info("mapper文件[" + configLocation.getFilename() + "]缓存加载成功");
} catch (IOException e) {
log.error("mapper文件[" + configLocation.getFilename() + "]不存在或内容格式不对");
continue;
}
}
}

return "刷新mybatis xml配置语句成功";
} catch (Exception e) {
e.printStackTrace();
return "刷新mybatis xml配置语句失败";
}
}

/**
* 扫描xml文件所在的路径
*
* @throws IOException
*/
private void scanMapperXml() throws IOException {
// 从sqlSessionFactoryBean中获取xml文件列表
Field privateField = ReflectionUtils.findField(sqlSessionFactoryBean.getClass(), "mapperLocations");
assert privateField != null;
ReflectionUtils.makeAccessible(privateField);
Resource[] field = (Resource[]) ReflectionUtils.getField(privateField, sqlSessionFactoryBean);
this.mapperLocations = field;

// 直接从服务器路径中获取xml文件列表
// this.mapperLocations = new PathMatchingResourcePatternResolver().getResources("file:/apps/6appform/appform/conf/mybatis/*.xml");
}

/**
* 清空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 = classConfig.getDeclaredField(fieldName);
field.setAccessible(true);
Map mapConfig = (Map) field.get(configuration);
mapConfig.clear();
}

@SuppressWarnings("rawtypes")
private void clearSet(Class<?> classConfig, Configuration configuration, String fieldName) throws Exception {
Field field = classConfig.getDeclaredField(fieldName);
field.setAccessible(true);
Set setConfig = (Set) field.get(configuration);
setConfig.clear();
}

/**
* 判断文件是否发生了变化
*
* @return
* @throws IOException
*/
private boolean isChanged() throws IOException {
boolean flag = false;
for (Resource resource : mapperLocations) {
String resourceName = resource.getFilename();

boolean addFlag = !fileMapping.containsKey(resourceName);// 此为新增标识

// 修改文件:判断文件内容是否有变化
Long compareFrame = fileMapping.get(resourceName);
long lastFrame = resource.contentLength() + resource.lastModified();
boolean modifyFlag = null != compareFrame && compareFrame.longValue() != lastFrame;// 此为修改标识

// 新增或是修改时,存储文件
if (addFlag || modifyFlag) {
fileMapping.put(resourceName, Long.valueOf(lastFrame));// 文件内容帧值
flag = true;
}
}
return flag;
}
}

springBean的方式,自动监听文件变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
  
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.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

import javax.annotation.PostConstruct;
import java.lang.reflect.Field;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Collectors;

@Component
@Slf4j
public class HotLoadingMybatisXMLPostConstructBean implements Ordered {


@Autowired
private SqlSessionFactory sqlSessionFactory;

@Autowired
private SqlSessionFactoryBean sqlSessionFactoryBean;


@PostConstruct
public void init() {
new WatchThread().start();
}


@Override
public int getOrder() {
return Integer.MAX_VALUE;
}

class WatchThread extends Thread {

private Resource[] mapperLocations;
private Configuration configuration;

@Override
public void run() {
startWatch();
}

/**
* 启动监听
*/
private void startWatch() {
try {
configuration = sqlSessionFactory.getConfiguration();
mapperLocations = (Resource[]) getFieldValue(sqlSessionFactoryBean, "mapperLocations");

WatchService watcher = FileSystems.getDefault().newWatchService();
getWatchPaths().forEach(p -> {
try {
Paths.get(p).register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
} catch (Exception e) {
log.error("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);
}
}

/**
* 加载需要监控的文件父路径
*/
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
*/
private Resource[] getResource() {
// 直接从服务器路径中获取xml文件列表
// Resource[] field = new PathMatchingResourcePatternResolver().getResources("file:/apps/6appform/appform/conf/mybatis/*.xml");
// 从sqlSessionFactoryBean中获取xml文件列表
return mapperLocations;
}

/**
* 删除xml元素的节点缓存
*
* @param nameSpace xml中命名空间
*/
private void clearMap(String nameSpace) {
log.info("清理Mybatis的namespace={}在mappedStatements、caches、resultMaps、parameterMaps、keyGenerators、sqlFragments中的缓存", nameSpace);
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 属性名称
*/
private Object getFieldValue(Object obj, String fieldName) {
log.info("从{}中加载{}属性", obj, fieldName);
try {
Field field = ReflectionUtils.findField(obj.getClass(), fieldName);
if (field != null) {
ReflectionUtils.makeAccessible(field);
return ReflectionUtils.getField(field, obj);
} else {
throw new RuntimeException("ERROR: 未加载到象中的[" + fieldName + "]属性");
}
} 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);
}
}
}
}

Written with StackEdit中文版.