安装JRebel热部署插件
MyBatis-Plus
在线安装JRebel mybatisPlus extension
插件商店直接搜JRebel mybatisPlus extension
如果在线安装无法使用,则离线安装JRebel mybatisPlus extension
《下载Zip离线安装JRebel mybatisPlus extension(MybatisPlus专用插件,对MyBatis不一定有用,MyBatis后面会用代码的方式实现)》
pom.xml
引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
修改SprngBoot启动配置
点击Modify options
选择update classes and resources【更新类和资源文件】
ok
JRebel启动SpringBoot项目
修改xml文件后按Ctrl + F10更新xml文件
控制台输出更新后的SQL,表示热部署正常
MyBatis
pom.xml
</dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<!--源生maven 打包排除MyBatis 热部署代码-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<excludes>
<!-- 要改成你自己的包路径 -->
<exclude>com/fu/demo/mybatishotreloader/</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
application.yml
mybatis:
mapper-locations: classpath*:mapper/*.xml
MyBatisHotReloaderThreadFactory.java
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class MyBatisHotReloaderThreadFactory implements ThreadFactory {
private final AtomicInteger nextId = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
String threadName = "mybatis-" + nextId.getAndIncrement();
Thread thread = new Thread(r, threadName);
thread.setDaemon(true);
return thread;
}
}
MybatisHotReloader.java
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.builder.ResultMapResolver;
import org.apache.ibatis.builder.annotation.MapperAnnotationBuilder;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.session.Configuration;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.util.ReflectionUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.*;
@Slf4j
@RequiredArgsConstructor
public class MybatisHotReloader implements InitializingBean, DisposableBean {
private final Resource[] mapperLocations;
private final Configuration configuration;
private WatchService watchService;
private ExecutorService executor;
private final Map<Path, byte[]> mostRecentFileHash = new ConcurrentHashMap<>();
private Collection<Class<?>> mapperClasses;
private static class DelayedPath implements Delayed {
public DelayedPath(Path path, long delayMilis) {
this.path = path;
this.endTime = System.currentTimeMillis() + delayMilis;
}
@Getter
private Path path;
private final long endTime;
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.endTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
DelayedPath other = (DelayedPath) o;
return Comparator.<DelayedPath>comparingLong(delayed -> delayed.endTime)
.thenComparing(DelayedPath::getPath)
.compare(this, other);
}
}
private final DelayQueue<DelayedPath> pathsToRevisit = new DelayQueue<>();
public void afterPropertiesSet() throws Exception {
addStatementLock();
this.mapperClasses = Collections.singletonList(this.configuration.getMapperRegistry().getMappers().getClass());
this.watchService = FileSystems.getDefault()
.newWatchService();
ThreadFactory threadFactory = new MyBatisHotReloaderThreadFactory();
this.executor = Executors.newSingleThreadExecutor(threadFactory);
for (Resource resource : mapperLocations) {
Paths.get(resource.getURI())
.getParent()
.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
}
this.executor.execute(() -> {
while (!Thread.interrupted()) {
this.reloadChangedFiles();
this.reloadDelayedPaths();
}
log.info("Stopped watching mappers");
});
}
private void reloadDelayedPaths() {
try {
List<DelayedPath> readyPaths = new ArrayList<>();
this.pathsToRevisit.drainTo(readyPaths);
for (DelayedPath path : readyPaths) {
this.reloadIfChanged(path.getPath(), true);
}
} catch (Exception e) {
log.error("Error while refreshing mappers", e);
}
}
private Object getLock() {
return this.configuration.getIncompleteResultMaps();
}
private void addStatementLock() {
this.configuration.getIncompleteResultMaps()
.add(new ResultMapResolver(null, null, null, null, null, null, false) {
@Override
@SneakyThrows
public ResultMap resolve() {
return null;
}
});
}
@SneakyThrows
private void reloadChangedFiles() {
WatchKey watchKey = watchService.poll(100, TimeUnit.MILLISECONDS);
if (watchKey == null) {
return;
}
try {
onPathChange(watchKey);
} catch (Exception e) {
log.error("Error while refreshing mappers", e);
} finally {
watchKey.reset();
}
}
@SneakyThrows
private void onPathChange(WatchKey watchKey) {
Path baseDir = (Path) watchKey.watchable();
for (WatchEvent<?> pollEvent : watchKey.pollEvents()) {
Path path = (Path) pollEvent.context();
Path absPath = baseDir.resolve(path).toAbsolutePath();
boolean reloadedFile = this.reloadIfChanged(absPath, false);
if (reloadedFile) {
return;
}
}
}
@SneakyThrows
private boolean reloadIfChanged(Path path, boolean reloadedBefore) {
try {
byte[] lastFileHash = mostRecentFileHash.getOrDefault(path, new byte[0]);
Optional<byte[]> currentFileHash = getPathContentHash(path);
if (!currentFileHash.isPresent()) {
log.trace("Empty mapper file <{}>", path);
if (!reloadedBefore) {
pathsToRevisit.add(new DelayedPath(path, 100));
}
return false;
}
if (Arrays.equals(lastFileHash, currentFileHash.get())) {
log.trace("Not reloading mapper - same file hash");
return false;
}
mostRecentFileHash.put(path, currentFileHash.get());
for (Resource resource : this.mapperLocations) {
Path resourceAbsPath = Paths.get(resource.getURI()).toAbsolutePath();
if (Objects.equals(resourceAbsPath, path)) {
log.info("Found mapper file to reload <{}>", resource.getURI());
this.reload();
return true;
}
}
} catch (FileNotFoundException e) {
// this may happen as file is first deleted from directory and then new one is written
log.trace("Mapper file not ready <{}>", path);
if (!reloadedBefore) {
pathsToRevisit.add(new DelayedPath(path, 100));
}
}
return false;
}
private Optional<byte[]> getPathContentHash(Path changedFileAbsPath) throws IOException, NoSuchAlgorithmException {
byte[] fileContent = Files.readAllBytes(changedFileAbsPath);
if (fileContent.length == 0) {
return Optional.empty();
}
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] hashBytes = digest.digest(fileContent);
return Optional.of(hashBytes);
}
private void reload() {
this.reloadAll();
}
@SneakyThrows
private void reloadAll() {
synchronized (this.getLock()) {
Thread.sleep(100);
this.clearOldConfiguration();
for (Resource resource : this.mapperLocations) {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(resource.getInputStream(),
configuration,
resource.toString(),
configuration.getSqlFragments());
xmlMapperBuilder.parse();
String namespace = getNamespace(xmlMapperBuilder);
configuration.addLoadedResource("namespace:" + namespace);
addStatementLock();
}
for (Class<?> mapperClazz : this.mapperClasses) {
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this.configuration, mapperClazz);
parser.parse();
}
log.info("All mapper files reloaded");
}
}
private String getNamespace(XMLMapperBuilder xmlMapperBuilder) {
Field builderAssistant = ReflectionUtils.findField(XMLMapperBuilder.class, "builderAssistant");
ReflectionUtils.makeAccessible(builderAssistant);
MapperBuilderAssistant assistant = (MapperBuilderAssistant) ReflectionUtils.getField(builderAssistant, xmlMapperBuilder);
return assistant.getCurrentNamespace();
}
private void clearOldConfiguration() {
this.clearFieldValue(configuration, "mappedStatements");
this.clearFieldValue(configuration, "caches");
this.clearFieldValue(configuration, "resultMaps");
this.clearFieldValue(configuration, "parameterMaps");
this.clearFieldValue(configuration, "keyGenerators");
this.clearFieldValue(configuration, "loadedResources");
this.clearFieldValue(configuration, "sqlFragments");
}
@SneakyThrows
private void clearFieldValue(Configuration configuration, String fieldName) {
Field field = ReflectionUtils.findField(configuration.getClass(), fieldName);
ReflectionUtils.makeAccessible(field);
Object fieldValue = ReflectionUtils.getField(field, configuration);
if (fieldValue instanceof Map) {
((Map<?, ?>) fieldValue).clear();
} else if (fieldValue instanceof Set) {
((Set<?>) fieldValue).clear();
} else {
throw new IllegalArgumentException("Field type not supported!");
}
}
@Override
public void destroy() throws IOException {
this.watchService.close();
this.executor.shutdown();
}
}
MybatisHotReloaderConfig.java
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisHotReloaderConfig {
private final MybatisProperties mybatisProperties;
public MybatisHotReloaderConfig(MybatisProperties mybatisProperties) {
this.mybatisProperties = mybatisProperties;
}
@Bean
public MybatisHotReloader mybatisHotReloader(SqlSessionFactory sqlSessionFactory) {
return new MybatisHotReloader(mybatisProperties.resolveMapperLocations(), sqlSessionFactory.getConfiguration());
}
}
老规矩和上面一样启动的时候设置更新类和资源
然后什么启动类加@MapperScan(“com.fu.demo.mapper”)扫描包就不说了