[问题]
相同的代码在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总是把父文件夹删了再新建