[问题]
相同的代码在eclipse中运行正常,在idea中不能重启,不能热加载
[项目开源地址]
https://gitee.com/litongjava_admin/hotswap-classloader
[idea版本]
IntelliJ IDEA 2019.3.3 (Ultimate Edition)
Build #IU-193.6494.35, built on February 11, 2020
[必要设置]
已经开启了Build project automaitcally和compiler.automake.allow.when.app.running
[问题分析]
无论怎么修改代码,保存,都无法触发spring-boot重启,猜测是没有检测到文件更改
在com.litongjava.hotswap.classloader.HotSwapWatcher#doRun增加日志输出

完整代码如下
package com.litongjava.hotswap.classloader;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.litongjava.hotswap.kit.UndertowKit;
import com.litongjava.hotswap.server.RestartServer;
/**
* 监听 class path 下 .class 文件变动,触发 UndertowServer.restart()
*/
public class HotSwapWatcher extends Thread {
protected RestartServer server;
// protected int watchingInterval = 1000; // 1900 与 2000 相对灵敏
protected int watchingInterval = 500;
protected List<Path> watchingPaths;
private WatchKey watchKey;
protected volatile boolean running = true;
public HotSwapWatcher(RestartServer server) {
setName("HotSwapWatcher");
// 避免在调用 deploymentManager.stop()、undertow.stop() 后退出 JVM
setDaemon(false);
setPriority(Thread.MAX_PRIORITY);
this.server = server;
this.watchingPaths = buildWatchingPaths();
}
protected List<Path> buildWatchingPaths() {
Set<String> watchingDirSet = new HashSet<>();
String[] classPathArray = System.getProperty("java.class.path").split(File.pathSeparator);
for (String classPath : classPathArray) {
buildDirs(new File(classPath.trim()), watchingDirSet);
}
List<String> dirList = new ArrayList<String>(watchingDirSet);
Collections.sort(dirList);
List<Path> pathList = new ArrayList<Path>(dirList.size());
System.out.println("观察的目录有:");
for (String dir : dirList) {
System.out.println(dir);
pathList.add(Paths.get(dir));
}
return pathList;
}
private void buildDirs(File file, Set<String> watchingDirSet) {
if (file.isDirectory()) {
watchingDirSet.add(file.getPath());
File[] fileList = file.listFiles();
for (File f : fileList) {
buildDirs(f, watchingDirSet);
}
}
}
public void run() {
try {
doRun();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
protected void doRun() throws IOException {
WatchService watcher = FileSystems.getDefault().newWatchService();
System.out.println("获取到的文件观察器是:"+watcher);
addShutdownHook(watcher);
for (Path path : watchingPaths) {
path.register(
watcher,
// StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE
);
}
while (running) {
try {
// watchKey = watcher.poll(watchingInterval, TimeUnit.MILLISECONDS); // watcher.take(); 阻塞等待
// 比较两种方式的灵敏性,或许 take() 方法更好,起码资源占用少,测试 windows 机器上的响应
watchKey = watcher.take();
if (watchKey == null) {
// System.out.println(System.currentTimeMillis() / 1000);
continue ;
}
} catch (Throwable e) { // 控制台 ctrl + c 退出 JVM 时也将抛出异常
running = false;
if (e instanceof InterruptedException) { // 另一线程调用 hotSwapWatcher.interrupt() 抛此异常
Thread.currentThread().interrupt(); // Restore the interrupted status
}
break ;
}
List<WatchEvent<?>> watchEvents = watchKey.pollEvents();
for(WatchEvent<?> event : watchEvents) {
Kind<?> kind = event.kind();
String fileName = event.context().toString();
System.out.println(watcher.toString()+"检测到文件修改"+kind.toString()+","+fileName);
if (fileName.endsWith(".class")) {
if (server.isStarted()) {
server.restart();
resetWatchKey();
while((watchKey = watcher.poll()) != null) {
// System.out.println("---> poll() ");
watchKey.pollEvents();
resetWatchKey();
}
break ;
}
}
}
resetWatchKey();
}
}
private void resetWatchKey() {
if (watchKey != null) {
watchKey.reset();
watchKey = null;
}
}
/**
* 添加关闭钩子在 JVM 退出时关闭 WatchService
*
* 注意:addShutdownHook 方式添加的回调在 kill -9 pid 强制退出 JVM 时不会被调用
* kill 不带参数 -9 时才回调
*/
protected void addShutdownHook(WatchService watcher) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
watcher.close();
} catch (Throwable e) {
UndertowKit.doNothing(e);
}
}));
}
public void exit() {
running = false;
try {
this.interrupt();
} catch (Throwable e) {
UndertowKit.doNothing(e);
}
}
// public static void main(String[] args) throws InterruptedException {
// HotSwapWatcher watcher = new HotSwapWatcher(null);
// watcher.start();
//
// System.out.println("启动成功");
// Thread.currentThread().join(99999999);
// }
}修改一个controller文件保存,发现输出如下

我修改的文件是com.litongjava.spring.boot.hello.HelloController216.java
但是却显示hello和boot的修改,boot和boot都是java文件的父目录
使用的文件监控技术是java的sun.nio.fs.WindowsWatchService
于是我猜测这是WindowsWatchService的一个小bug,于是我使用C#的FileSystemWatcher的技术监控文件的变化验证我的猜测
创建一个winform工程,窗口设计如下

组件如下
textPath:输入文件监控路径
btnWatchStart:开启监控,关闭监控按钮
btnClearLog:清除日志按钮
textLog:显示日志文本框
Form1.cs代码如下
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace winform_file_watcher {
public partial class frmMain : Form {
public frmMain() {
InitializeComponent();
}
private void btnWatchStart_Click(object sender, EventArgs e) {
FileWatchService f1 = new FileWatchService(@textPath.Text, textLog);
if (btnWatchStart.Text.Equals("开启监控")) {
f1.Start();
btnWatchStart.Text = "关闭监控";
} else {
f1.Stop();
btnWatchStart.Text = "开启监控";
}
}
private void btnClearLog_Click(object sender, EventArgs e) {
textLog.Text = "";
}
}
}FileWatchService代码如下
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace winform_file_watcher {
public class FileWatchService {
private FileSystemWatcher _watcher;
private TextBox _textLog;
public FileWatchService(string path, TextBox textLog) {
try {
this._watcher = new FileSystemWatcher();
_watcher.Path = path;
_watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.DirectoryName;
_watcher.IncludeSubdirectories = true;
_watcher.Created += new FileSystemEventHandler(FileWatcher_Created);
_watcher.Changed += new FileSystemEventHandler(FileWatcher_Changed);
_watcher.Deleted += new FileSystemEventHandler(FileWatcher_Deleted);
_watcher.Renamed += new RenamedEventHandler(FileWatcher_Renamed);
} catch (Exception ex) {
_textLog.AppendText("Error:" + ex.Message);
}
_textLog = textLog;
}
public void Start() {
this._watcher.EnableRaisingEvents = true;
_textLog.AppendText("文件监控已经启动...\r\n");
}
public void Stop() {
this._watcher.EnableRaisingEvents = false;
this._watcher.Dispose();
this._watcher = null;
_textLog.AppendText("文件监控已关闭...\r\n");
}
protected void FileWatcher_Created(object sender, FileSystemEventArgs e) {
_textLog.AppendText("新增:" + e.ChangeType + ";" + e.FullPath + ";" + e.Name+ "\r\n");
}
protected void FileWatcher_Changed(object sender, FileSystemEventArgs e) {
_textLog.AppendText("变更:" + e.ChangeType + ";" + e.FullPath + ";" + e.Name+ "\r\n");
}
protected void FileWatcher_Deleted(object sender, FileSystemEventArgs e) {
_textLog.AppendText("删除:" + e.ChangeType + ";" + e.FullPath + ";" + e.Name+ "\r\n");
}
protected void FileWatcher_Renamed(object sender, RenamedEventArgs e) {
var message=string.Format("重命名: OldPath:{0} NewPath:{1} OldFileName{2} NewFileName:{3}", e.OldFullPath, e.FullPath, e.OldName, e.Name+ "\r\n");
_textLog.AppendText(message);
}
}
}启动监控服务对比
在eclipse环境修改代码使用java得到的监控变动如下
sun.nio.fs.WindowsWatchService@702d719a检测到文件修改ENTRY_MODIFY,HelloController216.class
在idea环境修改代码使用java得到的监控变动如下
sun.nio.fs.WindowsWatchService@39f03030检测到文件修改ENTRY_MODIFY,hello sun.nio.fs.WindowsWatchService@39f03030检测到文件修改ENTRY_MODIFY,boot sun.nio.fs.WindowsWatchService@39f03030检测到文件修改ENTRY_CREATE,hello sun.nio.fs.WindowsWatchService@39f03030检测到文件修改ENTRY_MODIFY,hello
使用C#得到的监控变动如下
删除:Deleted;E:\dev_workspace\java\java-study\java-ee-spring-boot-study\java-ee-spring-boot-2.1.6-study\java-ee-spring-boot-2.1.6-hello\target\classes\com\litongjava\spring\boot\hello\HelloController216.class;com\litongjava\spring\boot\hello\HelloController216.class 删除:Deleted;E:\dev_workspace\java\java-study\java-ee-spring-boot-study\java-ee-spring-boot-2.1.6-study\java-ee-spring-boot-2.1.6-hello\target\classes\com\litongjava\spring\boot\hello;com\litongjava\spring\boot\hello 新增:Created;E:\dev_workspace\java\java-study\java-ee-spring-boot-study\java-ee-spring-boot-2.1.6-study\java-ee-spring-boot-2.1.6-hello\target\classes\com\litongjava\spring\boot\hello;com\litongjava\spring\boot\hello 新增:Created;E:\dev_workspace\java\java-study\java-ee-spring-boot-study\java-ee-spring-boot-2.1.6-study\java-ee-spring-boot-2.1.6-hello\target\classes\com\litongjava\spring\boot\hello\HelloController216.class;com\litongjava\spring\boot\hello\HelloController216.class 变更:Changed;E:\dev_workspace\java\java-study\java-ee-spring-boot-study\java-ee-spring-boot-2.1.6-study\java-ee-spring-boot-2.1.6-hello\target\classes\com\litongjava\spring\boot\hello\HelloController216.class;com\litongjava\spring\boot\hello\HelloController216.class
由C#得到的监控变更可知,idea在编译HelloController216.java时过程如下

[总结]
在idea中不能触发重启的原因是没有监控到class文件的改变
[解决办法]
如果使用idea开发,spring-boot使用controller对外提供重启的接口使用C#的winform程序监控文件的改变,监控到class文件改变发送请求到spring-boot的接口
不知道什么原因当目录只有一个.java文件的时候,idea总是把父文件夹删了再新建