使用Mybatis过程中,很多时候修改了XML文件需要整个项目重新启动,比较耗时,如果没什么业务数据状态还好,有数据状态可就惨啦,所以XML自动线下更新就很有必要。手写一个简单实现,大家参考下。
我的实现思路就是利用一个额外线程扫描mybatis
XML文件,更新到 Spring
中的 上下文ApplicationContext
中。
1. 配置文件
我们定义一套刷新时间和周期频次的配置文件在路径 persistence-mybatis\mybatis-base\src\main\resources\conf\mybatis-refresh.properties
中,里面内容如下:
enabled=true
delaySeconds=30
sleepSeconds=10
mappingPath=mapper
- enabled:是否开启自动刷新
- delaySeconds: 间隔时间
- sleepSeconds: 休眠时间
- mappingPath:XML的路径
核心类需要实现上下文接口 ApplicationContextAware
。
2. 关键步骤
- @Override重写
setApplicationContext
方法 - 用静态语句块,初始化配置文件中的相关参数
- @PostConstruct:在构造函数之后对
SqlSessionFactory
进行额外配置 - 启用线程按照频次间隔重复执行上述操作
关键性步骤如下:
// 1、从上下文容器获取 SqlSessionFactory
SqlSessionFactory sessionFactory = applicationContext.getBean(SqlSessionFactory.class);
// 2、获取Configuration
Configuration configuration = sessionFactory.getConfiguration();
this.configuration = configuration;
// 3、扫描Locations
mapperLocations = getResource(basePackage,XML_RESOURCE_PATTERN);
// 4、启动线程执行
exeTask();
核心类在akkad-base\persistence-mybatis\mybatis-base\src\main\java\xyz\wongs\drunkard\base\persistence\mybatis\loader\MapperAutoRefresh.java
下,而且行数太长,代码就不贴。
/**
* Mybatis的mapper文件中的sql语句被修改后, 只能重启服务器才能被加载, 非常耗时,所以就写了一个自动加载的类,
* 配置后检查xml文件更改,如果发生变化,重新加载xml里面的内容.
*
* @author <a href="https://github.com/rothschil">Sam</a>
* @date 20/11/17 10:29
* @since 1.0.0
*/
@Slf4j
@Component
@SuppressWarnings("unused")
public class MapperAutoRefresh implements ApplicationContextAware {
private ApplicationContext applicationContext;
private static final Properties prop = new Properties();
/**
* 是否启用Mapper刷新线程功能
*/
private static final boolean enabled;
/**
* 刷新启用后,是否启动了刷新线程
*/
private static boolean refresh;
/**
* Mapper实际资源路径
*/
private Set<String> location;
/**
* Mapper资源路径
*/
private Resource[] mapperLocations;
/**
* MyBatis配置对象
*/
private Configuration configuration;
/**
* 上一次刷新时间
*/
private Long beforeTime = 0L;
/**
* 延迟刷新秒数
*/
private static int delaySeconds;
/**
* 休眠时间
*/
private static int sleepSeconds;
/**
* xml文件夹匹配字符串,需要根据需要修改
*/
private static String mappingPath;
private static final String XML_RESOURCE_PATTERN = "**/*.xml";
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
static {
String FILE_NAME = "/conf/mybatis-refresh.properties";
try {
prop.load(MapperAutoRefresh.class.getResourceAsStream(FILE_NAME));
} catch (Exception e) {
log.error("Load mybatis-refresh “" + FILE_NAME + "” file error.");
}
enabled = ConstMapper.ENABLED_TRUE.equalsIgnoreCase(getPropString(ConstMapper.ENABLED));
delaySeconds = getPropInt(ConstMapper.DELAY_SECONDS);
sleepSeconds = getPropInt(ConstMapper.SLEEP_SECONDS);
mappingPath = getPropString(ConstMapper.MAPPING_PATH);
delaySeconds = delaySeconds == 0 ? 50 : delaySeconds;
sleepSeconds = sleepSeconds == 0 ? 3 : sleepSeconds;
mappingPath = StringUtils.isBlank(mappingPath) ? "mappings" : mappingPath;
if(log.isDebugEnabled()){
log.debug("[enabled] " + enabled);
log.debug("[delaySeconds] " + delaySeconds);
log.debug("[sleepSeconds] " + sleepSeconds);
log.debug("[mappingPath] " + mappingPath);
}
}
@PostConstruct
public void start() throws IOException {
SqlSessionFactory sessionFactory = applicationContext.getBean(SqlSessionFactory.class);
this.configuration = sessionFactory.getConfiguration();
String basePackage = "/mapper";
mapperLocations = getResource(basePackage, XML_RESOURCE_PATTERN);
exeTask();
}
/** 根据路径获取XML 的Resource
* @param basePackage 给定包
* @param pattern 正则表达式
* @return javax.annotation.Resource[]
* @date 20/11/17 10:48
*/
public Resource[] getResource(String basePackage, String pattern) throws IOException {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(applicationContext.getEnvironment().resolveRequiredPlaceholders(
basePackage)) + "/" + pattern;
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
return resourcePatternResolver.getResources(packageSearchPath);
}
class MyBatisThreadRefresh implements Runnable {
private final MapperAutoRefresh mapperAutoRefresh;
MyBatisThreadRefresh(MapperAutoRefresh mapperAutoRefresh) {
this.mapperAutoRefresh = mapperAutoRefresh;
}
@Override
public void run() {
// 解析资源
if (null == location) {
location = Sets.newHashSet();
log.debug("MapperLocation's length:" + mapperLocations.length);
for (Resource mapperLocation : mapperLocations) {
String s = mapperLocation.toString().replaceAll("\\\\", "/");
s = s.substring("file [".length(), s.lastIndexOf(mappingPath) + mappingPath.length());
if (!location.contains(s)) {
location.add(s);
log.info("Location:" + s);
}
}
log.info("Locarion's size:" + location.size());
}
// 暂定时间
try {
TimeUnit.SECONDS.sleep(delaySeconds);
} catch (InterruptedException e2) {
e2.printStackTrace();
}
refresh = true;
log.info("========= Enabled refresh mybatis mapper =========");
// 开始执行刷新操作
while (true) {
try {
for (String s : location) {
mapperAutoRefresh.refresh(s, beforeTime);
}
TimeUnit.SECONDS.sleep(sleepSeconds);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 执行资源刷新任务
*
* @date 20/11/17 11:04
*/
public void exeTask() {
if (null == mapperLocations || mapperLocations.length == 0) {
return;
}
beforeTime = System.currentTimeMillis();
if (enabled) {
// 启动刷新线程
final MapperAutoRefresh runnable = this;
ExecutorService es = ThreadPoolsUtil.doCreate(1, 1, "Mybatis-Refresh");
MyBatisThreadRefresh mtr = new MyBatisThreadRefresh(this);
es.execute(mtr);
}
}
/**
* 刷新资源的操作
*
* @param filePath 资源的路径
* @param beforeTime 开始时间
* @date 20/11/17 11:06
*/
public void refresh(String filePath, long beforeTime) {
// 本次刷新时间
long refrehTime = System.currentTimeMillis();
// 获取需要刷新的Mapper文件列表
List<File> fileList = this.getRefreshFile(new File(filePath), beforeTime);
if (fileList.isEmpty()) {
return;
}
log.info("Refresh file: " + fileList.size());
for (File file : fileList) {
try {
InputStream inputStream = new FileInputStream(file);
String resource = file.getAbsolutePath();
// 清理原有资源,更新为自己的StrictMap方便,增量重新加载
String[] mapFieldNames = new String[]{
"mappedStatements", "caches",
"resultMaps", "parameterMaps",
"keyGenerators", "sqlFragments"
};
for (String fieldName : mapFieldNames) {
Field field = configuration.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
Map map = ((Map) field.get(configuration));
if (!(map instanceof StrictMap)) {
Map newMap = new StrictMap(StringUtils.capitalize(fieldName) + "collection");
for (Object key : map.keySet()) {
try {
newMap.put(key, map.get(key));
} catch (IllegalArgumentException ex) {
newMap.put(key, ex.getMessage());
}
}
field.set(configuration, newMap);
}
}
// 清理已加载的资源标识,方便让它重新加载。
Field loadedResourcesField = configuration.getClass().getDeclaredField("loadedResources");
loadedResourcesField.setAccessible(true);
Set loadedResourcesSet = ((Set) loadedResourcesField.get(configuration));
loadedResourcesSet.remove(resource);
//重新编译加载资源文件。
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(inputStream, configuration,
resource, configuration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
e.printStackTrace();
} finally {
ErrorContext.instance().reset();
}
if (log.isDebugEnabled()) {
log.info("Refresh file: " + file.getAbsolutePath());
log.info("Refresh filename: " + file.getName());
}
if (!fileList.isEmpty()) {
this.beforeTime = refrehTime;
}
}
}
/**
* 获取需要刷新的文件列表,返回 刷新文件列表
*
* @param dir 目录
* @param beforeTime 上次刷新时间
* @return java.util.List<java.io.File>
* @date 20/11/17 11:18
*/
private List<File> getRefreshFile(File dir, Long beforeTime) {
List<File> fileList = new ArrayList<>();
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
fileList.addAll(this.getRefreshFile(file, beforeTime));
} else if (file.isFile()) {
if (this.checkFile(file, beforeTime)) {
fileList.add(file);
}
} else {
log.error("Error file." + file.getName());
}
}
}
return fileList;
}
/**
* 重写 org.apache.ibatis.session.Configuration.StrictMap 类
* 来自 MyBatis3.4.0版本,修改 put 方法,允许反复 put更新。
*/
public static class StrictMap<V> extends HashMap<String, V> {
private static final long serialVersionUID = -4950446264854982944L;
private final String name;
public StrictMap(String name, int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
this.name = name;
}
public StrictMap(String name, int initialCapacity) {
super(initialCapacity);
this.name = name;
}
public StrictMap(String name) {
super();
this.name = name;
}
public StrictMap(String name, Map<String, ? extends V> m) {
super(m);
this.name = name;
}
@SuppressWarnings("unchecked")
@Override
public V put(String key, V value) {
// ThinkGem 如果现在状态为刷新,则刷新(先删除后添加)
if (MapperAutoRefresh.isRefresh()) {
remove(key);
}
// ThinkGem end
if (containsKey(key)) {
throw new IllegalArgumentException(name + " already contains value for " + key);
}
if (key.contains(Constants.POINT)) {
final String shortKey = getShortName(key);
if (super.get(shortKey) == null) {
super.put(shortKey, value);
} else {
super.put(shortKey, (V) new Ambiguity(shortKey));
}
}
return super.put(key, value);
}
@Override
public V get(Object key) {
V value = super.get(key);
if (value == null) {
throw new IllegalArgumentException(name + " does not contain value for " + key);
}
if (value instanceof Ambiguity) {
throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
+ " (try using the full name including the namespace, or rename one of the entries)");
}
return value;
}
private String getShortName(String key) {
final String[] keyparts = key.split("\\.");
return keyparts[keyparts.length - 1];
}
protected static class Ambiguity {
private final String subject;
public Ambiguity(String subject) {
this.subject = subject;
}
public String getSubject() {
return subject;
}
}
}
public static boolean isRefresh() {
return refresh;
}
/**
* 判断文件是否需要刷新,需要刷新返回true,否则返回false
*
* @param file 文件
* @param beforeTime 上次刷新时间
* @return boolean
* @date 20/11/17 11:17
*/
private boolean checkFile(File file, Long beforeTime) {
return file.lastModified() > beforeTime;
}
/**
* 获取整数属性
*
* @param key KEY
* @return int
* @date 20/11/17 10:37
*/
private static int getPropInt(String key) {
return Integer.parseInt(Objects.requireNonNull(getPropString(key)));
}
/**
* 获取字符串属性
*
* @param key KEY
* @return int
* @date 20/11/17 10:37
*/
private static String getPropString(String key) {
return prop.getProperty(key);
}
}
在多线程处理这块有需要注意有一定的线程使用基础,看官自行学习。