Spring Boot DevTools 的热部署是怎么实现的?为什么用「两个类加载器」?
简化版
Spring Boot DevTools 是开发期的效率工具,核心是「自动重启(automatic restart)」——改了代码,不用手动停应用再启动,DevTools 会自动、快速地重启应用,让改动生效。它靠「两个类加载器」实现快速重启:DevTools 把类分成两拨——**① base classloader(基础类加载器)**加载「不会变的第三方 jar 依赖」(Spring、各种库);**② restart classloader(重启类加载器)**加载「你自己会频繁改的项目代码」。重启时,只丢弃并重建 restart classloader(重新加载你的代码),base classloader 和它加载的第三方库原样保留——因为第三方 jar 又多又大、加载慢,不用重新加载它们,重启就快得多(几乎「秒重启」,比冷启动快很多)。其他能力:文件变化自动触发重启、静态资源/模板改动实时生效(不用重启)、默认禁用模板缓存、配合 LiveReload 浏览器自动刷新。注意:DevTools 只用于开发,打成正式 jar 运行时会自动禁用(它检测到是完整打包运行就不启用)。
详细版
两个类加载器的分工:
base classloader(基础):
加载 依赖的第三方 jar(Spring、Hibernate、各种库)
→ 这些不会变,重启时保留、不重新加载
restart classloader(重启):
加载 你的项目代码(target/classes 下的类)
→ 这些频繁改,重启时丢弃旧的、用新的重新加载
重启时:
丢弃 restart classloader → new 一个新的 restart classloader
→ 重新加载你改过的项目代码
base classloader 不动(第三方库不重新加载)
→ 只重载"少量自己的代码",不重载"大量第三方库" → 快
DevTools 的主要能力:
| 能力 | 说明 |
|---|---|
| 自动重启(restart) | 项目代码变化 → 快速重启(两个类加载器) |
| 静态资源实时生效 | 改 HTML/CSS/JS、模板 → 不用重启就生效 |
| 禁用缓存 | 默认关掉模板引擎缓存(Thymeleaf 等),改了即时可见 |
| LiveReload | 内置服务器,配合浏览器插件自动刷新页面 |
| 全局配置 | ~/.spring-boot-devtools.properties |
| 远程调试支持 | 有限的远程更新能力(生产禁用) |
<!-- 引入 DevTools(optional=true 防止传递依赖到别的模块)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
⚠️ 要分清「DevTools 的自动重启」和「真正的热替换(HotSwap/JRebel)」——它们是两个层次:DevTools 的自动重启,本质仍是「重启应用」(只是靠双类加载器让重启很快),应用状态会丢失(内存里的数据清空、连接重建),它是「快速重启」不是「不重启改代码」。而 JRebel、HotSwapAgent 才是真正的「热替换」——在不重启 JVM的情况下替换已加载类的字节码(应用状态保留),靠 Java Agent + 字节码增强实现(见 Java Agent 相关题),能力更强但更复杂/收费。JVM 原生的 HotSwap 只支持「改方法体」,不支持加减方法/字段,能力有限。所以 DevTools 是「开发期快速重启」的轻量方案,不是「零重启改代码」。
完整版教学
一、DevTools 要解决什么:开发期重启慢
DevTools 解决的是「开发时改一点代码就要重启,太慢」:
开发时的痛点:
改一行代码 → 停应用 → 重新启动 → 等 Spring 容器初始化...
一次冷启动可能十几秒到几十秒(大项目)
→ 一天改几十次代码 = 大量时间浪费在等启动
想要的效果:
改了代码,应用"很快"就生效,不用等漫长的冷启动
DevTools 的方案:自动重启 + 加速重启
① 检测到代码变化,自动触发重启(不用手动操作)
② 用双类加载器让重启很快(不重新加载第三方库)
→ "改代码 → 几秒后生效",开发体验大幅提升
DevTools 的动机是「加速开发期的改代码-生效循环」——传统方式改代码要手动停启、冷启动十几秒到几十秒,一天几十次很浪费。DevTools 用「自动检测变化触发重启 + 双类加载器加速重启」把这个循环缩短到几秒。理解「DevTools 解决开发期重启慢、自动检测变化触发重启+双类加载器加速、改代码几秒生效」,就理解了它的定位。
二、核心原理:双类加载器加速重启
DevTools 快的关键是「双类加载器」——理解这点就理解了它的核心:
观察:一个应用启动慢,慢在哪?
大量时间花在"加载和初始化第三方库"(Spring、各种依赖 jar)
而你自己的项目代码 只占一小部分、加载快
DevTools 的洞察:既然第三方库不会变,重启时就别重新加载它们!
把类分成两拨,用两个类加载器:
base classloader → 加载第三方 jar(不变的,重启时保留)
restart classloader → 加载项目代码(会变的,重启时重建)
重启流程:
1. 检测到项目代码变化
2. 丢弃当前 restart classloader
3. new 一个新的 restart classloader,重新加载项目代码
4. base classloader 原样保留(第三方库不重新加载)
5. 重新触发 Spring 容器初始化(但类加载这块省了大头)
→ 因为跳过了"重新加载大量第三方库",重启快很多
DevTools 快的核心是「双类加载器:只重载会变的项目代码,保留不变的第三方库」——洞察是「启动慢主要慢在加载第三方库、而它们不会变」。把类分成 base classloader(第三方 jar,重启保留)和 restart classloader(项目代码,重启重建),重启时只丢弃重建 restart classloader、保留 base classloader,跳过重新加载大量第三方库,所以快。这也呼应了「类的身份 = 类名 + 类加载器」(换新类加载器就是换新类,见自定义类加载器题)。理解「双类加载器:base 加载第三方库(重启保留)+restart 加载项目代码(重启重建)、只重载会变的代码跳过第三方库所以快」,就理解了 DevTools 的核心原理。
三、自动重启的触发:文件监听
DevTools 怎么知道「代码变了、该重启了」?靠文件监听:
触发机制:
DevTools 监听 classpath 上的文件变化
当 classpath 下的文件更新(.class 文件被重新编译)→ 触发重启
关键:触发的是"编译后的 .class 变化",不是"你敲代码"
所以流程是:
你改 .java 代码 → IDE 自动编译成 .class(或手动 Build)
→ target/classes 下的 .class 更新
→ DevTools 检测到 → 自动重启
★ 如果 IDE 没开"自动编译",改了代码不生效——因为 .class 没更新
(IntelliJ 要开 Build project automatically / 或手动 Build)
哪些变化触发重启 vs 不触发:
触发重启:项目的 .class(Java 代码改动)
不触发重启(实时生效):静态资源、模板文件(见下节)
可配置:spring.devtools.restart.exclude 排除某些路径不触发重启
自动重启靠「监听 classpath 文件变化」——检测到 .class 文件更新就触发重启。关键:触发的是「编译后的 .class 变化」,所以要 IDE 开启自动编译(改了 .java 要编译成 .class 才触发;IntelliJ 需开「自动构建」或手动 Build,否则改代码不生效)。可用 spring.devtools.restart.exclude 排除某些路径。理解「DevTools 监听 .class 文件变化触发重启、要 IDE 开自动编译(否则 .class 不更新不生效)、可 exclude 排除路径」,就理解了自动重启的触发机制。
四、静态资源与模板:不重启实时生效
不是所有变化都需要重启——静态资源和模板可以「实时生效」:
两类变化的处理不同:
① 项目代码(.class)变化 → 需要重启(重载类)
② 静态资源/模板(HTML/CSS/JS/Thymeleaf 模板)变化 → 不用重启,实时生效
因为这些是"运行时读取的文件",不是"加载进 JVM 的类"
改了文件,下次请求读到的就是新的,无需重启
DevTools 的配套优化:
- 默认禁用模板引擎缓存(Thymeleaf/Freemarker 的 cache=false)
→ 否则模板被缓存了,改了也看不到
- LiveReload:DevTools 内置一个 LiveReload 服务器
配合浏览器的 LiveReload 插件,资源变化时自动刷新浏览器页面
→ 改 CSS → 页面自动刷新,所见即所得
所以:改后端代码 → 快速重启;改前端资源/模板 → 实时生效 + 自动刷新
DevTools 区分两类变化:项目代码(.class)需重启;静态资源/模板(HTML/CSS/模板文件)不用重启、实时生效(它们是运行时读取的文件,不是加载进 JVM 的类)。配套优化:默认禁用模板引擎缓存(否则改了模板看不到)、LiveReload(内置服务器 + 浏览器插件,资源变化自动刷新页面)。理解「静态资源/模板改动实时生效不用重启、DevTools 默认禁用模板缓存、LiveReload 自动刷新浏览器」,就理解了 DevTools 对前端资源的处理。
五、DevTools vs 真正的热替换(JRebel)
必须分清 DevTools 的「快速重启」和真正的「热替换」:
三个层次(能力从弱到强):
① JVM 原生 HotSwap(Debug 模式改方法体):
只能改"方法体内部",不能加/删方法、字段、类
能力很弱,但零成本(IDE Debug 时自动支持)
② DevTools 自动重启:
本质是"重启应用"(只是靠双类加载器让重启快)
★ 应用状态会丢失(内存数据清空、连接重建、Session 没了)
支持任意代码改动(加方法/字段/类都行,因为是重启)
免费、Spring Boot 自带
③ JRebel / HotSwapAgent(真正的热替换):
不重启 JVM,直接替换已加载类的字节码
★ 应用状态保留(内存数据还在)
靠 Java Agent + 字节码增强实现(见 Java Agent 题)
能力最强,但 JRebel 收费、配置复杂
对比记忆:
DevTools = "快速重启"(状态丢失,但重启快)
JRebel = "零重启热替换"(状态保留,能力强但收费)
区分三层次:① JVM 原生 HotSwap(只改方法体,能力弱但零成本);② DevTools 自动重启(本质是重启、状态会丢失,但双类加载器让重启快、支持任意改动、免费);③ JRebel/HotSwapAgent(真正热替换、不重启状态保留、靠 Java Agent + 字节码增强、能力强但 JRebel 收费)。核心区别:DevTools 是「快速重启」(状态丢失),JRebel 是「零重启热替换」(状态保留)。理解「三层次:JVM HotSwap(改方法体弱)/DevTools(快速重启但状态丢失)/JRebel(零重启热替换状态保留但收费)、DevTools 本质是重启不是热替换」,就理清了这个常被混淆的概念。
六、注意事项与生产禁用
DevTools 有几个使用注意点,尤其是「生产禁用」:
① 只用于开发,生产自动禁用:
DevTools 检测运行方式——如果是"完整打包的 jar 正常运行"
(java -jar app.jar),它会自动禁用自己
→ 所以引入 DevTools 不会影响生产(但仍建议 optional/scope 隔离)
② 依赖声明用 optional=true(Maven)或 developmentOnly(Gradle):
防止 DevTools 通过传递依赖"泄漏"到依赖你的其他模块
③ 状态丢失要有心理预期:
自动重启会清空内存状态(如内存里缓存的数据、H2 内存库数据)
→ 别指望重启后内存数据还在
④ 双类加载器偶尔的坑:
某些依赖强转、类加载相关的代码,
可能因"同一个类被不同 classloader 加载"出现 ClassCastException
→ 特定场景可用 spring.devtools.restart.enabled=false 关闭重启
⑤ 触发条件依赖 IDE 编译:改代码后要有 .class 更新才触发
DevTools 的注意点:① 生产自动禁用(检测到 java -jar 完整运行就不启用,但仍建议用 optional/developmentOnly 隔离依赖);② 依赖用 optional=true 防传递泄漏;③ 重启会丢失内存状态(内存缓存、H2 内存库数据清空);④ 双类加载器偶发 ClassCastException(同类被不同 classloader 加载);⑤ 依赖 IDE 编译出 .class 才触发。理解「DevTools 生产自动禁用、依赖用 optional 防泄漏、重启丢失内存状态、双类加载器偶发 ClassCastException、依赖 IDE 编译」,就掌握了实际使用的注意点。
记忆钩子:「Spring Boot DevTools = 开发期快速重启工具;核心双类加载器:base classloader 加载第三方 jar(不变,重启保留)+restart classloader 加载项目代码(会变,重启重建)→只重载自己的代码跳过第三方库所以快;监听 .class 变化触发重启(要 IDE 开自动编译);静态资源/模板实时生效不用重启(默认禁用模板缓存+LiveReload 自动刷新);DevTools 是’快速重启’(状态丢失)≠JRebel’零重启热替换’(状态保留但收费,靠 Java Agent);生产自动禁用,依赖用 optional」。
七、常见误区与追问
- 误区:DevTools 是「不重启就改代码」的热替换。 不是——DevTools 本质是「重启应用」(只是靠双类加载器让重启很快),应用内存状态会丢失;真正的「不重启热替换」是 JRebel/HotSwapAgent(靠 Java Agent 字节码增强,状态保留但收费)。
- 误区:DevTools 重启和冷启动一样慢。 快很多——冷启动要重新加载所有第三方库;DevTools 用双类加载器只重载会变的项目代码、保留不变的第三方库(base classloader),跳过了加载大量第三方库的耗时。
- 误区:改了代码 DevTools 就一定自动重启。 触发条件是「.class 文件更新」,要 IDE 开启自动编译(改 .java 编译成 .class)才触发;IntelliJ 需开「Build project automatically」或手动 Build,否则 .class 没变、不会重启。
- 误区:DevTools 会影响生产性能。 DevTools 检测到完整打包 jar 正常运行(java -jar)会自动禁用自己;且建议用 optional=true/developmentOnly 声明依赖,不会打进/影响生产。
- 追问:DevTools 为什么用两个类加载器? 因为「类的身份 = 类名 + 类加载器」,用一个专门的 restart classloader 加载会变的项目代码,重启时丢弃它、new 一个新的重新加载(得到「新版本的类」);而第三方库由 base classloader 加载、重启时保留不动,避免重新加载大量第三方库,从而快速重启。
- 追问:静态资源和 Java 代码改动的处理有什么不同? Java 代码(.class)改动需要重启(重载类);静态资源/模板(HTML/CSS/JS/模板文件)改动不用重启、实时生效(它们是运行时读取的文件,不是加载进 JVM 的类),DevTools 还默认禁用模板缓存并提供 LiveReload 自动刷新浏览器。
- 追问:DevTools 和 JRebel 怎么选? 一般开发用免费的 DevTools(快速重启够用,状态丢失可接受);如果项目巨大重启仍慢、或频繁调试需要保留应用状态(如调试一个复杂流程不想每次重来),可用 JRebel(零重启热替换、状态保留,但收费且配置复杂)。
八、加强记忆
Spring Boot DevTools 是开发期效率工具,核心是「自动重启」——改代码后自动、快速重启让改动生效。核心原理是「双类加载器」:① base classloader 加载不会变的第三方 jar(Spring、各种库,重启时保留);② restart classloader 加载你频繁改的项目代码(重启时丢弃并重建,重新加载你的代码)。重启时只重载「少量会变的项目代码」、跳过「重新加载大量第三方库」,所以比冷启动快很多(呼应「类身份 = 类名 + 类加载器」)。触发机制:监听 classpath 的 .class 文件变化(要 IDE 开自动编译才有 .class 更新)。静态资源/模板改动不用重启、实时生效(它们是运行时读取的文件,DevTools 默认禁用模板缓存 + 提供 LiveReload 自动刷新浏览器)。关键区分:DevTools 是「快速重启」(本质是重启,内存状态会丢失)≠ JRebel/HotSwapAgent 的「零重启热替换」(不重启、状态保留,靠 Java Agent + 字节码增强,能力强但收费)。DevTools 生产自动禁用(检测到 java -jar 完整运行不启用),依赖用 optional=true/developmentOnly 隔离。一句话「DevTools 靠双类加载器(base 加载第三方库重启保留 + restart 加载项目代码重启重建)实现快速重启,监听 .class 变化(需 IDE 自动编译);静态资源/模板实时生效+LiveReload;它是快速重启(状态丢失)不是 JRebel 那种零重启热替换;生产自动禁用」。