DevTools 热重启
概述
Spring Boot DevTools(spring-boot-devtools)提供了一套开发期增强工具,核心功能包括自动重启(Restart)和LiveReload。当 classpath 中的文件发生变化时,DevTools 通过双重类加载器机制快速重启应用(仅加载修改的应用类,第三方依赖保持不变),并通过 LiveReload 协议通知浏览器自动刷新。
本文将深入拆解 DevTools 热重启的 10 个内部细节,涵盖自动配置条件、文件监控、类加载器实现、LiveReload 推送等核心内容。
本文基于 Spring Boot 3.2.5 +
spring-boot-devtools3.x 源码分析。
1. LocalDevToolsAutoConfiguration 的条件
LocalDevToolsAutoConfiguration 是 DevTools 本地热重启功能的自动配置入口。
@AutoConfigureAfter(DevToolsPropertyDefaultsPostProcessor.class)
// 默认启用,可通过 spring.devtools.restart.enabled=false 关闭
@ConditionalOnProperty(prefix = "spring.devtools.restart", name = "enabled", matchIfMissing = true)
public class LocalDevToolsAutoConfiguration {
@Bean
public Restarter restart() {
// 注册 Restarter 到 BeanFactory
return Restarter.getInstance();
}
// FileSystemWatcher 工厂
@Bean
public FileSystemWatcherFactory fileSystemWatcherFactory() {
return new FileSystemWatcherFactory() {
@Override
public FileSystemWatcher createFileSystemWatcher() {
return new FileSystemWatcher(true, Duration.ofMillis(400), Duration.ofMillis(100));
}
};
}
// 可选:LiveReload Server
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.devtools.livereload", name = "enabled", matchIfMissing = true)
static class LiveReloadConfiguration {
// ...
}
}触发条件:
| 条件 | 说明 |
|---|---|
@ConditionalOnClass(spring-boot-devtools.jar) | classpath 中存在 DevTools |
@ConditionalOnProperty("spring.devtools.restart.enabled") | 默认启用(matchIfMissing=true) |
非远程应用(RemoteDevToolsAutoConfiguration 除外) | 本地开发环境 |
| 非测试环境 | 通过 DevToolsApplicationStateFilter 排除测试运行 |
关闭 DevTools:
# application.properties
spring.devtools.restart.enabled=false # 关闭自动重启
spring.devtools.livereload.enabled=false # 关闭 LiveReload2. DevToolsPropertyDefaultsPostProcessor 的默认配置
DevToolsPropertyDefaultsPostProcessor 是一个 EnvironmentPostProcessor,在环境准备阶段应用一系列开发期友好的默认配置。
public class DevToolsPropertyDefaultsPostProcessor implements EnvironmentPostProcessor, Ordered {
// 映射表:dev 环境下关闭缓存的 key
private static final Map<String, Object> DEFAULT_PROPERTIES;
static {
Map<String, Object> defaults = new LinkedHashMap<>();
// 模板引擎缓存(开发时关闭)
defaults.put("spring.freemarker.cache", "false");
defaults.put("spring.thymeleaf.cache", "false");
defaults.put("spring.groovy.template.cache", "false");
defaults.put("spring.mustache.cache", "false");
defaults.put("spring.velocity.cache", "false");
defaults.put("spring.velocity.properties.input.encoding", "UTF-8");
// 静态资源缓存(开发时关闭)
defaults.put("spring.web.resources.cache.period", "0");
// 其他开发期优化
defaults.put("spring.web.resources.chain.cache", "false");
defaults.put("spring.web.resources.chain.strategy.content.enabled", "false");
defaults.put("spring.web.resources.chain.strategy.content.paths", "");
DEFAULT_PROPERTIES = Collections.unmodifiableMap(defaults);
}
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
// 仅当 DevTools 在 classpath 中且应用不是通过 spring-boot-maven-plugin 打包的
if (isDevToolsPresent() && isLocalApplication(application)) {
// 将这些默认值添加到 Environment 尾部(优先级最低,可被用户配置覆盖)
PropertySource<?> propertySource = new MapPropertySource("devtools", DEFAULT_PROPERTIES);
environment.getPropertySources().addLast(propertySource);
}
}
}默认关闭的缓存:
| 配置项 | 默认值 | 说明 |
|---|---|---|
spring.freemarker.cache | false | Freemarker 模板缓存 |
spring.thymeleaf.cache | false | Thymeleaf 模板缓存 |
spring.groovy.template.cache | false | Groovy 模板缓存 |
spring.mustache.cache | false | Mustache 模板缓存 |
spring.web.resources.cache.period | 0 | 静态资源缓存有效期 |
spring.web.resources.chain.cache | false | 资源链缓存 |
DevTools 自动关闭这些缓存,确保开发者修改模板或静态资源后能立即看到效果,无需手动配置。
3. FileSystemWatcher 的 WatchService 轮询
FileSystemWatcher 使用 Java NIO 的 WatchService 监控文件系统变化。
public class FileSystemWatcher {
private final WatchService watchService;
private final boolean daemon;
private final Duration quietPeriod;
private final Duration pollInterval;
private final Set<File> folders;
private final List<FileChangeListener> listeners = new ArrayList<>();
public FileSystemWatcher(boolean daemon, Duration quietPeriod, Duration pollInterval) {
this.daemon = daemon;
this.quietPeriod = quietPeriod;
this.pollInterval = pollInterval;
try {
this.watchService = FileSystems.getDefault().newWatchService();
} catch (IOException ex) {
throw new IllegalStateException("无法创建 WatchService", ex);
}
}
// 注册被监控的目录
public void addFolder(File folder) {
this.folders.add(folder);
registerFolder(folder);
}
private void registerFolder(File folder) {
Path path = folder.toPath();
try {
// 注册 MODIFY 事件(文件修改)
path.register(this.watchService,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE);
} catch (IOException ex) {
// ...
}
}
// 扫描线程持续轮询
public void start() {
Thread thread = new Thread(this::scan);
thread.setDaemon(this.daemon);
thread.setName("File Watcher");
thread.start();
}
private void scan() {
while (true) {
// 轮询 WatchKey
WatchKey key = this.watchService.poll(this.pollInterval.toMillis(), TimeUnit.MILLISECONDS);
if (key == null) continue;
// 处理事件
List<ChangedFile> changes = new ArrayList<>();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
Path modifiedPath = (Path) event.context();
changes.add(new ChangedFile(folder, modifiedPath.toFile()));
}
}
// 通知监听器(需经过 quiet period 防抖)
if (!changes.isEmpty()) {
notifyListeners(changes);
}
key.reset();
}
}
}监控机制:
FileSystemWatcher 线程
↓
for (;;) ← 循环轮询
↓
WatchService.poll(100ms) ← 每 100ms 检查一次
↓
有事件? → 收集变更文件 → 等待 quietPeriod → 通知监听器
↓
无事件? → 继续轮询4. FileSystemWatcher 的 quietPeriod=400ms 防抖
quietPeriod(静默期)是 DevTools 防止频繁重启的关键机制。当文件变更事件触发时,FileSystemWatcher 会等待 quietPeriod 期间无新事件才通知重启。
public class FileSystemWatcher {
private final Duration quietPeriod; // 默认 400ms
private void notifyListeners(List<ChangedFile> files) {
// 进入 quiet period
// 如果在 quietPeriod 期间又有新的文件变更事件,则重置计时器
while (true) {
long start = System.currentTimeMillis();
// 等待 quietPeriod 时长
Thread.sleep(quietPeriod.toMillis());
// 检查 quietPeriod 期间是否有新事件
WatchKey key = watchService.poll(Duration.ZERO);
if (key == null) {
break; // 安静期已过,无新事件 → 触发重启
}
// 有新事件 → 继续等待(重置 quiet period)
key.pollEvents();
key.reset();
}
// 静默期结束后,触发重启
for (FileChangeListener listener : this.listeners) {
listener.onChange(files);
}
}
}防抖示意图:
时间线:
t0 用户保存文件(触发 ENTRY_MODIFY)
t1 FileSystemWatcher 捕获到事件
t2 开始 quiet period(400ms)
t3 用户再次保存文件(修改了另一个文件) ← 新事件
t4 重置 quiet period(重新开始 400ms)
t5 无新事件发生
t6 quiet period 结束 → 触发重启为什么需要 400ms 防抖:
- IDE 保存文件时可能会触发多次文件修改事件(如 IntelliJ IDEA 的 safe write)
- 构建工具可能会同时修改多个文件(如 Maven
process-classes编译输出) - 避免在文件尚未完全写入时提前触发重启
5. Restarter 的 RestartClassLoader 实现
Restarter 是热重启的核心类,通过 RestartClassLoader 实现应用类的隔离加载。
public class Restarter {
private static Restarter instance;
private final RestartClassLoader restartClassLoader;
private final Thread mainThread;
private final Set<URL> urls; // 应用类路径
private final Set<URL> frameworkUrls; // 框架类路径
private Restarter(String mainClassName, ClassLoader classLoader) {
// 1. 分离应用类路径和框架类路径
this.urls = new LinkedHashSet<>();
this.frameworkUrls = new LinkedHashSet<>();
for (URL url : ((URLClassLoader) classLoader).getURLs()) {
if (isFrameworkUrl(url)) {
this.frameworkUrls.add(url);
} else {
this.urls.add(url);
}
}
// 2. 创建 RestartClassLoader(父加载器 = 框架类加载器)
this.restartClassLoader = createRestartClassLoader();
// 3. 保存主线程引用
this.mainThread = Thread.currentThread();
}
}RestartClassLoader 源码:
public class RestartClassLoader extends URLClassLoader {
public RestartClassLoader(URL[] urls, ClassLoader parent) {
// 父加载器 = 框架类加载器(仅加载框架类)
// 自身 URL[] = 应用类(BOOT-INF/classes/)
super(urls, parent);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 检查已加载的类
Class<?> loaded = findLoadedClass(name);
if (loaded != null) return loaded;
// 2. 尝试从应用类路径加载(RestartClassLoader 自身)
try {
return findClass(name);
} catch (ClassNotFoundException ignored) {}
// 3. 委托给父加载器(框架类加载器)
return super.loadClass(name, resolve);
}
}双重类加载器架构:
Application ClassLoader(系统类加载器)
↓ 初始加载
Restarter 初始化
↓ 分离
├── RestartClassLoader (应用类) ← 每次重启新建
│ ├── com.example.demo.*
│ ├── BOOT-INF/classes/
│ └── src/main/resources/
│
└── Framework ClassLoader (框架类) ← 重启复用
├── spring-boot-*.jar
├── spring-core-*.jar
├── tomcat-embed-*.jar
└── BOOT-INF/lib/*.jar重启时:创建新的 RestartClassLoader,旧的 RestartClassLoader 被 GC 回收。
6. RestartLauncher 新线程启动
RestartLauncher 负责在新线程中重新启动 Spring Boot 应用。
public class Restarter {
public void restart() {
// 1. 停止旧上下文
stop();
// 2. 创建新的 RestartClassLoader
RestartClassLoader newClassLoader = new RestartClassLoader(
this.urls.toArray(new URL[0]),
this.frameworkClassLoader
);
// 3. 启动 RestartLauncher 线程
RestartLauncher launcher = new RestartLauncher(newClassLoader);
launcher.start();
// 4. 等待新应用启动完成
try {
launcher.join(); // 阻塞,直到新应用启动完成或失败
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
private void stop() {
// 关闭旧的 ApplicationContext
ConfigurableApplicationContext oldContext = getCurrentContext();
if (oldContext != null) {
oldContext.close();
}
// 清除旧的 RestartClassLoader
System.gc(); // 提示 GC 回收旧类加载器
}
}RestartLauncher 实现:
class RestartLauncher extends Thread {
private final RestartClassLoader classLoader;
RestartLauncher(RestartClassLoader classLoader) {
super("restart-main");
this.classLoader = classLoader;
setContextClassLoader(classLoader); // 设置上下文类加载器
}
@Override
public void run() {
try {
// 通过新的 RestartClassLoader 加载主类
Class<?> mainClass = classLoader.loadClass(getMainClassName());
// 反射调用 SpringApplication.run()
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[]{getArgs()});
} catch (Exception ex) {
// 启动失败处理
log.error("热重启失败", ex);
}
}
}重启流程:
文件变更 → FileSystemWatcher 触发
↓
Restarter.restart()
↓
① 关闭旧 ApplicationContext
② 创建新 RestartClassLoader
③ 启动 RestartLauncher 线程
④ 新线程内调用 SpringApplication.run()
↓
新应用启动完成
↓
旧 RestartClassLoader → GC7. LiveReloadServer 的 WebSocket 推送
DevTools 内嵌了一个 LiveReloadServer,在资源变更时通过 WebSocket 通知浏览器自动刷新。
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.devtools.livereload", name = "enabled", matchIfMissing = true)
static class LiveReloadConfiguration {
@Bean
@ConditionalOnMissingBean
LiveReloadServer liveReloadServer(FileSystemWatcherFactory fileSystemWatcherFactory) {
return new LiveReloadServer(fileSystemWatcherFactory);
}
}LiveReloadServer 实现:
public class LiveReloadServer {
private final ServerSocket serverSocket; // 监听 35729 端口
private final List<Connection> connections = new CopyOnWriteArrayList<>();
public LiveReloadServer(FileSystemWatcherFactory fileSystemWatcherFactory) {
try {
// LiveReload 协议默认端口 35729
this.serverSocket = new ServerSocket(35729);
} catch (IOException ex) {
throw new IllegalStateException("LiveReload 服务器启动失败", ex);
}
// 监听文件变化 → 通知所有连接
FileSystemWatcher watcher = fileSystemWatcherFactory.createFileSystemWatcher();
watcher.addListener(files -> triggerReload());
watcher.start();
// 启动连接接收线程
Thread connectionThread = new Thread(this::acceptConnections);
connectionThread.setDaemon(true);
connectionThread.start();
}
private void acceptConnections() {
while (true) {
Socket socket = serverSocket.accept();
Connection connection = new Connection(socket);
connections.add(connection);
// 处理 LiveReload 协议握手(handshake)
connection.handshake();
}
}
public void triggerReload() {
// 向所有连接的浏览器发送 reload 命令
for (Connection connection : connections) {
connection.send("reload"); // LiveReload 协议: "hello" → "reload"
}
}
}LiveReload 协议流程:
浏览器(已安装 LiveReload 插件)
↓
WebSocket 连接 ws://localhost:35729/livereload
↓
Server → Client: "!!ver:1.6" ← 版本协商
Client → Server: "hello" ← 客户端握手
Server → Client: "hello" ← 服务器响应
↓
文件变更时
Server → Client: "reload" ← 推送刷新命令
Client (浏览器) → 自动刷新页面配置:
# LiveReload 端口
spring.devtools.livereload.port=35729
# 关闭 LiveReload
spring.devtools.livereload.enabled=false8. TriggerFile 文件名设置
默认情况下,DevTools 监控 classpath 中所有文件的变更。TriggerFile 允许通过特定文件的修改来触发重启,而不必监控所有文件。
public class FileSystemWatcherFactory {
private final String triggerFile;
public FileSystemWatcher createFileSystemWatcher() {
FileSystemWatcher watcher = new FileSystemWatcher(true, quietPeriod, pollInterval);
// 如果设置了 trigger file,则只监控 trigger file 的变化
if (this.triggerFile != null) {
watcher.setTriggerFile(triggerFile);
}
return watcher;
}
}
public class FileSystemWatcher {
private String triggerFile;
public void setTriggerFile(String triggerFile) {
this.triggerFile = triggerFile;
}
private void notifyListeners(List<ChangedFile> files) {
if (this.triggerFile != null) {
// 只检查 trigger file 是否被修改
boolean triggerChanged = files.stream()
.anyMatch(f -> f.getFile().getName().equals(this.triggerFile));
if (!triggerChanged) {
return; // trigger file 未变更,跳过重启
}
}
// 继续 quiet period 检测
// ...
}
}配置方式:
# application.properties
spring.devtools.restart.trigger-file=.reloadtrigger使用场景:
# 1. 在项目根目录创建 trigger 文件(无需提交到 Git)
touch .reloadtrigger
# 2. 想要触发热重启时,修改该文件
echo "$(date)" > .reloadtrigger优点:
- 避免大型项目中不必要的重启
- 只在开发者准备就绪时才触发重启
- 适用于使用外部编译工具(如 Gradle 守护进程)的项目
9. ExcludeFilter 的排除路径
Restarter 通过 ExcludeFilter 排除特定路径,避免不必要的重启触发和类加载。
public class Restarter {
private static final Set<String> DEFAULT_EXCLUDE_PATTERNS = new LinkedHashSet<>(
Arrays.asList(
"/META-INF/maven/**", // Maven 元数据
"/META-INF/resources/**", // 资源文件
"/META-INF/spring/", // Spring 配置
"/spring-boot-autoconfigure-*.jar", // Spring Boot 自动配置
"/spring-boot-*.jar", // Spring Boot 核心
"/spring-*.jar", // Spring Framework
"/spring-boot-devtools-*.jar", // DevTools 自身
"/tomcat-embed-*.jar", // 内嵌容器
"/jackson-*.jar", // Jackson
"/log4j-*.jar", // 日志
"/logback-*.jar", // 日志
"/slf4j-*.jar" // 日志
)
);
public static boolean isExcluded(URL url) {
String path = url.getPath();
for (String pattern : DEFAULT_EXCLUDE_PATTERNS) {
if (path.contains(pattern.replace("*", "").replace("/", ""))) {
return true;
}
}
return false;
}
}排除路径分类:
| 类别 | 排除模式 |
|---|---|
| 构建元数据 | /META-INF/maven/** |
| Web 资源 | /META-INF/resources/** |
| 框架类 | /spring-boot-*.jar、/spring-*.jar |
| 自动配置 | /spring-boot-autoconfigure-*.jar |
| 内嵌容器 | /tomcat-embed-*.jar |
| 序列化 | /jackson-*.jar |
| 日志 | /log4j-*.jar、/logback-*.jar、/slf4j-*.jar |
自定义排除:
# application.properties
spring.devtools.restart.exclude=static/**,public/**10. Restarter.initialize() 在 main() 方法拦截
Restarter 通过 Java Agent 或 SpringApplication 的钩子在应用启动前拦截 main() 方法,完成类加载器初始化。
public class Restarter {
public static void initialize(String[] args) {
// 通过栈帧找到 main 方法所在的类
StackTraceElement[] stack = new Exception().getStackTrace();
String mainClassName = null;
for (StackTraceElement element : stack) {
if ("main".equals(element.getMethodName())) {
mainClassName = element.getClassName();
break;
}
}
// 创建 Restarter 实例
if (instance == null) {
instance = new Restarter(mainClassName, getClassLoader());
}
}
private static ClassLoader getClassLoader() {
// 获取系统类加载器
return ClassLoader.getSystemClassLoader();
}
}SpringApplication 中的拦截:
public class SpringApplication {
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
// 如果在 DevTools 环境下运行,Restarter 已在之前初始化
return new SpringApplication(primarySource).run(args);
}
}DevTools 的 main() 拦截机制:
DevTools 使用了 DevToolsApplicationStateFilter 来确保 Restarter 在 SpringApplication.run() 调用之前完成初始化:
public class DevToolsApplicationStateFilter {
static final String KEY = "spring.devtools.application.running";
public static void apply(ApplicationContext context, SpringApplication springApplication, String[] args) {
// 1. 确保 Restarter 已初始化
if (isLocalApplication(springApplication)) {
Restarter.initialize(args);
}
// 2. 注册关闭钩子(清理 LiveReload、WatchService)
registerShutdownHook(context);
}
}完整的启动拦截流程:
java -jar my-app.jar
↓
JVM 启动 → 加载系统类加载器
↓
main() 方法调用
↓
DevToolsApplicationStateFilter.apply() ← 拦截点
↓
Restarter.initialize()
↓
① 创建 RestartClassLoader(分离应用类和框架类)
② 保存主类名和参数
③ 启动 FileSystemWatcher 线程
↓
SpringApplication.run()
↓
应用正常启动
↓
后续文件变更 → Restarter.restart() → 新线程重启远程 DevTools 的不同:
远程应用(RemoteDevToolsAutoConfiguration)通过 HTTP 传输文件变更,而非本地文件监控:
本地 IDE ↔ HTTP ↔ 远程服务器
↓
RemoteRestartClient.sendFiles()
↓
远程 Restarter.restart()
↓
使用 RemoteRestartClassLoader 加载类总结
DevTools 热重启的 10 个内部细节点总结如下:
| # | 细节点 | 核心类/机制 |
|---|---|---|
| ① | LocalDevToolsAutoConfiguration 的条件 | @ConditionalOnProperty("spring.devtools.restart.enabled") 默认启用 |
| ② | DevToolsPropertyDefaultsPostProcessor 的默认配置 | spring.freemarker.cache=false / spring.thymeleaf.cache=false / spring.web.resources.cache.period=0 |
| ③ | FileSystemWatcher 的 WatchService 轮询 | StandardWatchEventKinds.ENTRY_MODIFY 事件 |
| ④ | FileSystemWatcher 的 quietPeriod=400ms 防抖 | 400ms 内无事件才触发重启 |
| ⑤ | Restarter 的 RestartClassLoader 实现 | URLClassLoader 子类,只重新加载应用类 |
| ⑥ | RestartLauncher 新线程启动 | new Thread(() -> SpringApplication.run(...)).start() |
| ⑦ | LiveReloadServer 的 WebSocket 推送 | LiveReload 协议 hello → reload 通知浏览器 |
| ⑧ | TriggerFile 文件名设置 | spring.devtools.restart.trigger-file=.reloadtrigger |
| ⑨ | ExcludeFilter 的排除路径 | /META-INF/maven/** / /META-INF/resources/** / /spring-boot-autoconfigure-*.jar |
| ⑩ | Restarter.initialize() 在 main() 方法拦截 | DevToolsApplicationStateFilter 钩子拦截 SpringApplication.run() |