Spring Boot 开发中有一个很常见的场景:改完一个 Service 方法后,如果不重启,接口返回的仍然是旧逻辑。手动重启通常需要十秒到几十秒,而 Spring Boot DevTools 可以在几秒内完成自动重启。它的核心并非字节码热替换,而是把项目类与依赖类拆分成两个加载器,每次只重建项目的那个部分。

这种设计大幅缩短了等待时间,但也决定了它仍然属于重启范畴,而不是完全无感的热部署。接下来从类加载器机制、依赖配置、触发策略和 IDE 配合几个角度展开。
一、双类加载器机制:为什么秒级就能恢复上下文
应用正常启动时,JVM 会把所有类按需加载到方法区。DevTools 接入后,Spring Boot 会把类加载器拆成 base 和 restart 两个层次。base 类加载器加载来自 Maven 或 Gradle 仓库的依赖,例如 spring-webmvc、commons-lang 这些稳定不变的 jar 包;restart 类加载器则负责加载 target/classes 或 build/classes 下的项目类。项目类会因为开发修改而频繁变化,所以每次变更只需要丢弃 restart 类加载器,再重新扫描项目 class 路径即可。
可以打印一个控制器类的类加载器来验证。启用 DevTools 后,大部分项目类会由 org.springframework.boot.devtools.restart.classloader.RestartClassLoader 加载,而 String、List 等 JDK 类仍然由 bootstrap 类加载器负责。这样的隔离让基础依赖不需要重新装载,节省了类加载和部分初始化开销。Spring 容器会重新刷新,但并不是所有对象都从零开始创建,一些基础 Bean 的元数据仍然可以复用。
@RestController
public class LoaderController {
@GetMapping("/loader")
public String loader() {
return LoaderController.class.getClassLoader().getClass().getName();
}
}
这个接口返回的通常是 RestartClassLoader 的类名。需要注意的是,如果修改了方法体内部的逻辑,类名不变,但类加载器会换成新的实例。旧实例被丢弃后由 GC 回收。因此它并不能保留对象运行时的现场,像 JRebel 那样做到方法体替换,这也是后面要讨论的边界所在。
二、依赖引入与常用配置
要让项目启用 DevTools,Maven 工程只需在 pom.xml 中加入一个依赖,并且建议同时声明 optional 为 true。optional 的作用是阻止这个依赖传递到其他模块或可执行 jar 中,避免开发工具混入生产部署。scope 设为 runtime 是因为编译期不需要引用 DevTools 的类,它只在运行期介入。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
Gradle 工程可以使用 developmentOnly 来声明同等语义,这个配置不会把依赖打进正式产物。如果使用 Spring Boot Gradle 插件,也可以直接用 bootRun 配合 devtools。
dependencies {
developmentOnly 'org.springframework.boot:spring-boot-devtools'
}
配置文件中最常用的控制项是 spring.devtools.restart.enabled,默认为 true,可以设置为 false 关闭自动重启。另一个常被关心的参数是 spring.devtools.restart.exclude,它用来指定哪些路径变化时不触发重启。关于排除路径,后续触发策略一节会展开。还可以通过 spring.devtools.restart.additional-paths 把非 classpath 的目录纳入监听范围,比如某些资源目录被单独放在工程外时就会用到。
spring.devtools.restart.enabled=true spring.devtools.restart.exclude=static/**,public/** spring.devtools.restart.additional-paths=src/main/resources
这些属性能够应对大部分后端开发场景。实际工作中,配置不生效往往不是因为属性写错,而是 IDE 没有把修改后的 class 文件输出到 DevTools 监听的目录。因此依赖配置完成之后,还需要确认 IDE 的编译行为。
三、触发条件、排除目录与触发文件
DevTools 监听的是 classpath 条目的变更。默认情况下,很多静态资源目录已经被排除在重启触发之外,比如 /resources、/static、/public、/templates。因为前端静态资源通常希望直接热加载而不是重启整个 Spring 上下文。但如果你的项目把模板或自定义静态资源放在其他目录,就需要追加到 spring.devtools.restart.exclude 里,避免改一个 HTML 就触发一次重启,增加无谓等待。
对于需要控制重启时机的场景,可以使用 trigger-file。比如某些外部工具会不断修改 classpath 文件,但你不想每次修改都触发重启,就可以指定一个触发文件。只有这个文件发生变化时,DevTools 才会执行重启。典型配置如下:
spring.devtools.restart.trigger-file=.triggerfile
手工触发时可以执行 touch .triggerfile 生成新时间戳,这样比直接修改代码文件更可控。还有全局配置文件 .spring-boot-devtools.properties,可以放在用户主目录下,这样多个项目共享默认配置。例如公司内部统一把自动重启关闭或统一排除目录,就不需要每个工程重复写相同属性。
另一个容易混淆的点是 additional-paths。它不会把你指定的目录纳入 classpath,而是额外增加一个监控路径。例如 src/main/resources 默认已经监控,但在某些多模块结构中,配置目录可能不在当前模块 classpath 下,此时可以手动添加。理解了监听与触发的关系,排查为什么不重启和为什么频繁重启就会轻松很多。
四、IDE 配合与实际使用边界
IDE 是 DevTools 使用体验的关键一环。IntelliJ IDEA 默认不会在应用运行时自动编译修改后的类,需要开启 Build Project Automatically,并在注册表中允许 automake 在应用运行时工作。Eclipse 通常会自动编译保存的文件,体验相对顺畅。很多开发者配置了 DevTools 却看不到重启效果,问题多半出在 IDE 没有把新 class 写到输出目录,而不是依赖或 YAML 配置错误。
还有一个常见的误解是把 DevTools 当成 JRebel 或 DCEVM 这类真正的热替换工具。DevTools 可以自动重启、可以刷新静态资源,但遇到修改方法签名、改变继承关系、调整注解元数据等结构变化时,它仍然需要重建类加载器和 Spring 上下文。方法体内部的简单修改虽然也要重启,但时间被控制得很短。如果希望连方法体修改都不丢失状态,那需要引入字节码热替换方案,而不是继续叠加 DevTools 配置。
最后要强调生产安全。spring-boot-devtools 的 optional 和 scope 设置意味着它不应该被传递到最终部署包。官方也保证从完整打包的可执行文件启动时 DevTools 默认禁用。如果发现生产日志中出现了 DevTools 相关输出,首先检查构建链路是否把开发环境依赖打进去了。将重启关闭、排除目录设置好后,DevTools 会成为开发阶段非常实用的效率工具,而不是一个不稳定的黑盒。
Spring Boot DevTools热部署自动重启修改时间:2026-09-26 05:16:16