我最早接触 Spring Boot 热部署是在一个内部管理系统的开发阶段。项目里有二十几个 Controller、七八套数据库表每次改完一个接口的 SQL手动 CtrlR 重启等 Spring Boot 容器把数据源、事务、定时任务全部初始化一遍少说十几秒多则半分钟。一天改三十次这代价肉眼可见。后来团队引进了 Spring Boot DevTools以为从此可以“改了代码马上生效”结果第一天就遇到“加了依赖完全不重启”的怪事。把问题理清楚之后我发现大部分人对 DevTools 的理解都停留在表面有人把它当成 Spring 的 HotSwap有人以为它会自动替换所有 class还有人在生产服务器上也尝试开远程重启。这篇文章我就从原理到坑完整梳理一遍包括双类加载器怎么工作、文件监听如何触发、IntelliJ 里两个容易漏掉的开关以及一个完整的排查案例。如果你正在用 Spring Boot又纠结要不要上 devtools或者已经加了依赖但没感受过“自动重启”这篇文章应该对你有用。1. DevTools 给你做的三件事自动重启、缓存关停、LiveReload 联动1.1 自动重启它是“重启应用”不是“替换类”DevTools 的自动重启本质上仍然是“重启 Spring 上下文”。它检测到 classpath 下的 class 文件发生变化之后会重建类加载器重新扫描组件重新初始化 ApplicationContext。也就是说Spring 容器的组件扫描、自动配置、Bean 创建这些流程一个都不会少只是跳过了第三方依赖的类加载过程。很多人把它叫热部署容易让人联想到 Java 的 HotSwap以为能像前端一样改完立刻生效。实际上它更像“快速冷启动”重启动期间会有一个短暂不可用的窗口。只是本地开发环境没有并发请求通常感知不到。但如果你在调试长事务、定时任务就有可能在它重启的间隙里遇到状态中断这个需要心里有数。1.2 缓存关停模板引擎先进入开发模式Spring Boot 默认会对 Thymeleaf、Freemarker 等模板引擎开缓存目的是生产环境减少磁盘 I/O。这个缓存一开你改了模板刷新页面看到的还是旧内容。DevTools 在启动时会自动把spring.thymeleaf.cache、spring.freemarker.cache等属性改成 false所以很多人加了 devtools 后模板立刻实时生效。但要注意它只处理 Spring Boot 自动配置里默认控制的缓存。你在业务代码里自己构建的 Caffeine、手动 Map 缓存它一概不管。这个问题我会在第五章专门展开讲因为实际项目里八成的不生效都是这个原因。1.3 LiveReload浏览器端也自动刷新第三件事是内嵌了一个 LiveReload 服务器默认监听 35729 端口。浏览器装上 LiveReload 插件后会跟 devtools 建立 WebSocket 连接。当模板、静态资源变化时虽然应用不会重启但插件会收到通知并自动刷新页面。所以改完静态资源后你连手动按 F5 都可以省了。功能触发动作实际效果注意点自动重启classpath 下 class 文件变化应用自动重启不是 JVM HotSwap缓存关停启动时自动注入默认属性模板引擎即时生效业务缓存不受影响LiveReload静态资源、模板变化浏览器自动刷新需要插件且 35729 端口可用把这三件事分开理解特别重要。很多人把“缓存关停”当成“自动重启”的附带效果或者觉得改了模板也应该触发重启。实际上模板在默认排除列表里不会触发重启只会触发浏览器刷新。这里的边界是class 变了走重启静态资源变了走 LiveReload两者配合起来才是完整的开发体验。2. 双类加载器架构为什么 DevTools 重启比冷启动快又为什么依赖 jar 不归它管2.1 baseClassLoader 与 restartClassLoader 的分工DevTools 启动应用时会把类路径分成两条线。第三方依赖Spring 框架、数据库驱动、各种 jar 包交给 baseClassLoader 加载项目自己编译出来的 class 由 restartClassLoader 加载。重启发生时老的 restartClassLoader 被丢弃重新 new 一个 restartClassLoader 去加载当前项目最新的 class而 baseClassLoader 原封不动复用。因为项目自身的代码通常只是依赖总量里的一小部分所以重启成本远低于冷启动。打个比方整个应用是一家饭店后厨baseClassLoader 是厨房里的灶台、冰箱、水电管路这些设备不会轻易换restartClassLoader 是厨师今天要切的菜菜不新鲜了重新切一份灶台不动。DevTools 干的事就是只换“菜”不动“灶台”。2.2 FileSystemWatcher 监听链条与默认排除目录devtools 的触发源头是 Spring Framework 的 FileSystemWatcher。它并不是操作系统事件机制而是按固定间隔轮询 classpath 所在目录的文件状态识别新增、删除、修改。所以你看到的自动重启其实是这条完整链路文件系统变化 → 轮询发现 → 构造新 restartClassLoader → 重建 ApplicationContext。默认排除路径说明/META-INF/mavenMaven 元数据/META-INF/resources打包资源/resources开发期资源目录/static静态资源/public静态资源/templates模板文件这些路径下的内容变化不会触发应用重启但部分会被 LiveReload 捕捉用来刷新页面。假如工程结构特殊需要自定义排除可以用spring.devtools.restart.exclude如果需要额外监听不在 classpath 里的目录用spring.devtools.restart.additional-paths。2.3 两个被忽略的结论第一个结论所有以 jar 依赖形式进来的模块都不归 restartClassLoader 管。你如果在一个多模块工程里把公共模块打成了 jar 并 install 到本地仓库然后让主模块依赖这个 jar那么修改公共模块源码后即使你重新 install 并触发 devtools 重启加载的仍然是旧代码必须冷启动。这个结论后面排查案例会用到。第二个结论devtools 的自动重启并不是字节码级热替换。它在同一个 JVM 进程里重建 Spring 上下文所以被新上下文接管的 bean 会重新初始化但进程之外的状态它管不到进程内部一些跨上下文的状态也可能残留。明白了这两点很多“不生效”的问题都能快速定位。3. 加了依赖却不重启九成是 IDE 自动构建和运行中 make 没配好3.1 Maven 依赖先写对runtime、optional 不是可选项dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-devtools/artifactId scoperuntime/scope optionaltrue/optional /dependency官方推荐把 scope 设成 runtime、optional 设为 true。原因很直接devtools 只在本地开发时需要它不应该进入可执行 jar也不应该被传给下游模块。optionaltrue 会阻止 Maven 把它传递出去Spring Boot 的 repackage 在默认情况下也会把 devtools 排除掉。如果你不加 optional打出来的 jar 和其他模块的依赖关系都容易被污染。在 IDE 里跑 spring-boot:run 或者 main 方法时只要依赖加载正常DevTools 就会被识别。3.2 IntelliJ 两个开关社区版也一样配置如果你把依赖加进去了启动也没报错但改代码就是不自动重启先查 IntelliJ 的两个位置。第一个是 Settings → Build, Execution, Deployment → Compiler → Build project automatically勾上它。第二个是 CtrlShiftAlt/ 打开 Registry搜compiler.automake.allow.when.app.running把它也勾上。前一个负责允许自动构建项目后一个负责在应用正在运行的时候也执行自动构建。为什么需要后一个IntelliJ 出于性能考虑在应用进程还在跑的时候不会自动触发编译除非你点了 Build 或手动按 CtrlF9。devtools 的 FileSystemWatcher 监听的是 class 文件IDE 不重新编译class 文件不变自然永远等不到重启。社区版用户不用担心这些开关不依赖 Spring 插件是编译器层面的通用功能。新版 IntelliJ 在不断调整选项位置如果你在 Registry 里搜不到compiler.automake.allow.when.app.running去 Settings 的 Compiler 面板找找类似“allow auto-make to start even if the application is currently running”的选项逻辑是一样的。3.3 CtrlR、IDE HotSwap 和 DevTools 的区别顺手说一个常见混淆。很多人开发时习惯了两个刷新行为浏览器里按 CtrlR 是刷新页面IDEA 里按 CtrlR 是重新运行整个应用。浏览器刷新只能反映静态资源和模板变化后端 class 代码的变化落在 devtools 上如果你直接按 CtrlR 重新运行那 devtools 的自动重启价值就被绕过了。它省的就是你手动触发重启的那一下。另一个容易混淆的是 IntelliJ 自带的 HotSwap。你在 Debug 模式下改了方法体里的一段逻辑IDEA 可能直接通过 JVM 的 HotSwap 在运行中的进程里替换字节码不需要重启。但它只能处理方法体级别的修改新增方法、改字段、改类签名都不支持这时 IDEA 会提示 cannot reload。所以你会遇到一种奇妙的现象某些改动不用重启就生效某些改动自动重启也生效不了。这两套机制经常同时存在能判断是哪一层在生效是调试的基本功。4. 排查实录从“改了代码没反应”到定位 IntelliJ 隐藏开关4.1 第一步确认 DevTools 到底启动没有我之前帮同事排查过一个 Spring Boot MyBatis 的项目现象很简单pom 里加了 devtools控制台没有任何异常但无论怎么改 Controller、Service应用都纹丝不动。我第一步不是去翻代码而是看启动日志里有没有 LiveReload server running on port 35729 这行输出。没有。这说明 devtools 压根没有参与应用运行。再往下查发现他用的 Maven 仓库里确实下载了 jar但 IDE 运行配置使用的 classpath 并没有把它加进来——重新刷新 Maven 项目后这步解决了。怎么验证 devtools 真的生效除了启动日志另一个可靠办法是在代码里临时引用 devtools 的类比如org.springframework.boot.devtools.restart.Restarter能编译能运行说明依赖已经加载。如果日志和类都正常再走下一步。4.2 第二步理清构建链路大多数问题卡在“没编译”确认 devtools 启动了以后把整条触发链画出来源码改动 → IDE 自动编译 → target/classes 下 class 文件变化 → FileSystemWatcher 轮询到变化 → 重建 restartClassLoader → Spring 上下文重启。链条上的第一环就经常断也就是 IDE 的自动构建没开。具体到刚才那个案例他的 Settings 里 Build project automatically 是勾上的但 Registry 里的compiler.automake.allow.when.app.running没有勾导致应用运行期间 IDE 根本不重新编译。我把这一项勾上回到代码里加了一行日志应用立刻自动重启控制台出现 restarting 字样。注意修改文件不要只改注释或换行这种改动如果编译后 class 文件内容不变devtools 不会触发。它不是“假的没生效”而是真的没有变化可以监听。4.3 第三步重启真的发生了再排查缓存与旧状态还有一种更隐蔽的情况重启日志出现了结果接口返回的还是旧逻辑。这种时候先别怀疑 devtools去检查是不是命中缓存。我在另一个项目上遇到过Service 方法加了本地缓存改了方法内部实现devtools 确实重启了但重启后第一次请求从 Redis 里拿到旧值导致明明代码逻辑已经变了客户端看到的还是老数据。处理起来很简单开发环境把业务缓存关掉或者每次重启后手动清理相关 key。如果断点调试发现走的是新代码但输出结果不符合预期再考虑外部依赖的旧连接、残留线程这类脏状态。排查顺序很重要先确认 devtools 是否启用再确认 IDE 是否编译接着确认是否缓存拦截最后才考虑是不是类加载器层面的问题。4.4 修复与固化把经验写进团队规范把开关打开之后这个同事后来很少再遇到不重启的问题。我顺手帮他把 pom 里 devtools 写规范并把 IDE 自动构建的配置截图放进了项目的 README。对于多人协作的 Spring Boot 工程热部署配置最好形成固定约定否则每个新人都可能卡在同一个坑里。相比花一晚上排查提前花十分钟把环境约束好收益大得多。5. DevTools 管不到的业务缓存、静态状态和连接风暴5.1 外部缓存不会因重启消失Redis 旧值是“假不生效”高发区DevTools 重启 Spring 容器内存里的单例 Bean 会重新创建本地 Caffeine 这类内存缓存通常会随之重建。但是 Redis、数据库状态这些外部存储不会因为应用重启自动清空。你在本地改完接口逻辑Redis 里还躺着旧 key接口返回的当然是旧值。这种情况最容易被误判成“热部署失效”。我的建议是开发环境统一把业务缓存关掉或者给缓存 key 加一个 dev 环境专用前缀。如果你依赖 Redis 里的某些预热数据至少要在 devtools 重启后加一个手动清理入口不然排查时特别容易绕远路。判断方法也简单——在方法入口打断点看是走进新分支还是直接命中缓存返回。5.2 同一 JVM 重启的遗留状态线程、静态引用与类加载器泄漏devtools 在同一 JVM 内重启一个容易被忽略的问题是老 restartClassLoader 并不是立刻就被回收。如果某些线程、监听器或者静态引用还握着旧类加载器里的对象旧 ClassLoader 就会一直存活。重复重启几十次之后Metaspace 里的类元数据会缓慢堆积某些场景下还会出现诡异的 ClassCastException因为同一个类在不同类加载器里被加载了两遍。这不是说 devtools 有严重缺陷而是它的工作方式决定了一定会有这种边界情况。我的经验是如果当天改代码触发了几十次重启临近下班最好手动冷启动一次把可能积压的类加载器和线程状态清干净。遇到奇怪的反射异常、代理对象类型不匹配时先别死磕重启一次往往就好了。5.3 用 trigger-file 控制重启节奏保护本地中间件连接如果你所在的工程依赖一堆中间件比如本地开发连着一个远程测试库的 Redis、RabbitMQ、Nacos每次自动重启都会把这些外部连接重新建立一遍。代码改得越频繁连接闪断越明显有时候重启几十次之后中间件后台会积累大量孤立连接。这时候可以用 trigger-file 来控制重启触发spring.devtools.restart.trigger-file.reloadtrigger设置之后只有当你手动改了.reloadtrigger这个文件devtools 才会执行重启。在 Linux/Mac 下执行touch .reloadtriggerWindows 下新建或改内容是常用操作。这样做的代价是失去全自动的体验换来的是可控、低打扰的重启节奏。适合大型项目或者你在调一些和外部强交互的代码时使用。6. 生产环境里的 DevTools、Remote 模式和 JRebel 等替代方案6.1 打包成 jar 后 devtools 为什么会自动退出很多刚接触 devtools 的人会担心代码里绑了这么个自动重启的东西生产环境会不会哪天触发一次诡异重启。答案是它在生产环境根本不会跑。通过 spring-boot:repackage 生成的可执行 jar在打包阶段默认就会把 devtools 排除掉退一步说即使你强行把它保留在包里它的自动重启基于 restartClassLoader这种类加载器只在 IDE 或 spring-boot:run 的开发模式下才能发挥作用java -jar 运行时不会启用。所以生产环境的热更新走的是灰度发布、滚动重启那套流程而不是 devtools。如果你希望线上服务具备更灵活的发布能力应该关注的是 DevOps 流水线、负载均衡摘流、容器编排这类基础设施而不是在代码里埋一个开发期工具。6.2 Remote DevTools远程开发没问题但生产别开DevTools 还提供了一个 Remote 模式允许远程连接正在运行的开发实例通过 HTTP 触发重启。它需要设置spring.devtools.remote.secret之类的鉴权信息默认是关闭的。这个功能在远程开发、容器内开发的场景下有一定价值但它的本质是暴露一个可以远程操控应用生命周期的入口。生产环境开 Remote DevTools 属于高危操作等于给线上服务留了一个可以随时被触发重启的后门。就算加了 secret也不建议碰。线上排查问题请走日志、Metrics、链路追踪远程作业平台不要贪图开发期工具的便利。6.3 JRebel 与 IDE HotSwap三种方案怎么取舍JRebel 的核心机制和 DevTools 完全不同它通过 Java agent 在 JVM 层面对已加载的类做字节码重定义不需要重启 Spring 上下文也不需要重建类加载器。方法体变化、方法签名变化都能更平滑地处理所以大型多模块项目里体验确实更好。但它需要商业授权团队引入前要评估预算。方案代价核心机制适用场景DevTools免费需配置重启 Spring 上下文单体、中小项目JRebel商业授权Java agent 字节码重定义大型多模块、复杂依赖IDE HotSwap免费方法体级 JVM redefine调试局部逻辑工程里如果同时开 devtools 和 JRebel会出现两类加载机制互相干扰的问题一般建议只保留一个。如果你只是想让本地开发少按几次重启devtools 完全够用如果几乎每几分钟就要改一次 Service 层方法签名再考虑商业方案。这个话题还经常和 Spring Boot 的生产监控混淆。想看线上应用健康状态不是靠热部署工具而是走 Spring Boot Actuator 暴露指标再用 Spring Boot Admin 做聚合展示。热部署是开发期体验监控是运维期能力两回事。最后再分享一个我现在的固定习惯把 devtools 当成开发期基础设施来配而不是临时小插件。pom 里写好 runtime optionalIntelliJ 的两个自动构建开关开机先检查启动日志里见到 LiveReload server running on port 35729 才往下走大型项目用 trigger-file 控制重启频率一天临近收工时冷启动一次把可能积压的类加载器和线程状态清干净。这套组合我用下来基本再没被热部署问题困扰过。如果你也踩过类似的坑按这个顺序排查大概率能省下半天时间。
阅读完成 · 觉得有帮助?