Bootstrap

JRebel热部署SpringBoot+MyBatis+MyBatis-Plus实现不重启更新修改后MyBatis的XML文件

安装JRebel热部署插件

《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”)扫描包就不说了

;