写代码最怕什么?我估计十个人里有八个会说:改一行代码,切回 IDEA,重启 Spring Boot,然后盯着启动日志转圈圈。小项目还好,几十秒就回来了;项目一旦上了规模,光启动就要两三分钟,一天改几十次代码,光等重启的时间就够让人崩溃。所以“热加载方案”真不是炫技,而是为了不加班、不心烦,把时间花在真正要解决的问题上。
这篇文章我把 IDEA 里跑 Spring Boot 项目最常用的三种热加载方案一次性讲透:Spring Boot DevTools、IDEA 自带的 Debug Hot Swap、以及 JRebel 插件。三者的配置成本从零到几分钟不等,热替换能力也从“改改方法体”到“新增一个类都能不重启”逐级递增。不管你是刚接触 Spring Boot 的新人,还是被启动速度折磨许久的老人,这篇文章都值得看完。
1. 先搞清楚:为什么需要热加载?三种方案到底在“热”什么?
1.1 手动重启与热加载的本质区别
很多人把“热加载”和“自动重启”混为一谈,其实它俩不是一个东西。
Spring Boot DevTools 的默认行为,本质上是自动重启。它检测到 classpath 下的文件变化,帮你直接重启整个应用。虽然省去了手动点重启按钮的动作,但应用的启动时间一点没少,只是从“你等”变成了“它自己等”。
而真正的热加载,指的是在 JVM 不停止的情况下,替换掉已经加载的类字节码。这样不需要重新走一遍 Bean 初始化、Spring 容器装配、连接池建立这些流程,改动一编译,新代码立刻生效。DevTools 做不到这一点,IDEA 自带的 Hot Swap 只能做很有限的热替换,JRebel 才是真正意义上的全量热加载。
理解这个区别很重要,因为很多人用了 DevTools 之后觉得“也就那样,启动还是要二十秒”,其实是选错了方案。
1.2 三种热加载方案的能力边界
我把三种方案的差异做了一个对比,大家先建立整体认知:
| 方案 | 配置成本 | 方法体修改 | 新增方法/字段 | 新增类 | Spring 配置变更 | 对启动时间的影响 |
|---|---|---|---|---|---|---|
| Spring Boot DevTools | 低(一个依赖) | 支持(重启生效) | 支持(重启生效) | 支持(重启生效) | 支持(重启生效) | 重启时间取决于项目规模 |
| IDEA Hot Swap | 零(Debug 自带) | 支持(即时生效) | 不支持 | 不支持 | 不支持 | 无重启,即时生效 |
| JRebel | 中(插件+授权) | 支持(即时生效) | 支持(即时生效) | 支持(即时生效) | 大部分支持 | 无重启,即时生效 |
注意一个关键点:DevTools 在表格里看起来“什么都能改”,但它是通过重启实现的,不是同一个内存空间里的热替换。如果你只是想改一行日志输出,DevTools 也得重启一次,而 Hot Swap 和 JRebel 是真正的不重启秒生效。
后面的章节,我会把三种方案从接入到实战逐个拆开讲,每一步都给出可以直接照抄的配置。
2. 方案一:Spring Boot DevTools——官方免费,最适合日常开发
2.1 接入 DevTools 只需三步
DevTools 是 Spring Boot 官方提供的开发期增强工具,目前依然是大多数团队的首选方案。接入步骤非常简单,我直接给配置。
第一步,在pom.xml里加依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency>这里有两个细节要解释一下。scope用runtime,意思是编译期不需要,运行期才需要,避免开发时误用它的 API 到业务代码里。optional设为true也很关键,这样项目被其他模块依赖时,devtools 不会被传递出去,防止生产环境把开发工具带进去。
第二步,改代码后编译一次,让 class 文件发生变化。IDEA 里按Ctrl+F9(Build Project),DevTools 检测到 classpath 变化,就会自动重启应用。控制台会输出类似这样的日志:
Restarting Spring Boot application...第三步,如果你希望连 IDEA 都不切,直接改代码就自动编译,可以在 IDEA 设置里打开自动编译。路径是File -> Settings -> Build, Execution, Deployment -> Compiler -> Build project automatically,勾选上。再进高级设置Advanced Settings,把Allow auto-make to start even if developed application is currently running也勾上。这样你修改完文件,IDEA 会自动编译,DevTools 会自动重启,手基本不需要离开键盘。
2.2 让 DevTools 真正好用的几个配置
默认状态下的 DevTools 能用,但有几个点不优化的话,体验会打折扣。
第一个是静态资源的干扰。如果你在项目里同时写前端页面,改一下 HTML、JS、CSS 也会触发 DevTools 重启,而这类资源其实刷新浏览器就够了。解决办法是在application.properties里排除掉:
spring.devtools.restart.exclude=static/**,public/**,templates/**这样静态资源的改动不会触发重启,改完直接Ctrl+Shift+F9编译一下,浏览器刷新就能看到效果,省掉大量无意义的重启。
第二个是模板缓存。如果你用的是 Thymeleaf,默认生产环境下会开模板缓存,开发环境需要关掉,不然改了模板页面刷新还是老内容:
spring.thymeleaf.cache=false spring.resources.cache-period=0我见过不少同事第一次用 DevTools,改模板没反应,以为 DevTools 坏了,其实就是模板缓存没关。
第三个是触发文件。在大项目里,经常会遇到“改一个依赖的静态资源,结果触发了好几次重启”的情况。DevTools 支持配置一个触发文件,只有这个文件变化,才真正执行重启。做法是在application.properties里加:
spring.devtools.restart.trigger-file=.trigger然后在resources目录下新建一个名为.trigger的空文件。以后你改代码、编译,DevTools 不会立即重启,只有当你手动更新.trigger文件的时间戳时,才会触发重启。这个机制特别适合那些“依赖包内部文件变动频繁”或者“远程调试时不想频繁重启”的场景。
2.3 DevTools 的原理与局限性,值得你知道
用 DevTools 之前,我建议你了解一下它的类加载机制,否则遇到奇怪问题容易懵。
DevTools 会把项目里的类分成两组:base classloader 加载第三方依赖 jar,restart classloader 加载你自己写的类。每次重启,它会创建新的 restart classloader,新的类加载器去加载工作区里最新编译出的 class 文件,而第三方依赖的类还是老的 base classloader 里的,不用重新加载。
这个机制带来两个好处:第一,重启速度比完整冷启动快,因为省掉了一大堆 jar 包的类加载和扫描;第二,很聪明地避免了“依赖没变却要重复加载”的浪费。但代价是,如果你在本地临时改了某个 jar 里的类,DevTools 不会感知到,因为它始终从依赖仓库里加载。
局限性也很明显:说到底还是重启,Spring 容器、数据库连接池、Redis 连接、定时任务调度器都会全部重建。项目如果启动要 40 秒,DevTools 重启也要 35 秒左右,省不了多少。它只是把“点按钮重启”这件事自动化了。
我记得一个真实的踩坑经历:之前有个定时任务在项目启动时初始化了一批内存数据,我用 DevTools 改完代码后,这批数据会重新初始化,但缓存里对旧对象的引用还在。排查了很久才发现是重启后容器重建,但外部缓存没有跟着清。所以遇到“重启后状态不一致”的诡异问题,先往容器重建方向想。
3. 方案二:IDEA 自带的 Hot Swap——零配置,但大多数人没用好
3.1 Hot Swap 到底是什么
如果你用的是 IDEA 社区版或者公司不让你装额外插件,又想不重启改代码,其实 IDEA 自带了一个被低估的能力:Debug 模式下的 Hot Swap(Java 虚拟机热替换)。
Hot Swap 的底层是 JVM 的数HotSpot虚拟机提供的RedefineClasses能力,简单说就是允许你在运行中的 JVM 里,用新的 class 字节码替换掉已经加载的类。这是 JDK 官方支持的机制,不需要任何插件。
但 Hot Swap 有一个铁律:它只能替换方法体。换句话说,你可以在方法里面加一行日志、减一个判断、改一个返回值类型,这些都能生效;但你不能新增一个方法、不能增加或删除字段、不能修改类的继承关系,这些属于结构的改变,JVM 不允许在运行期替换。
Java 21 之后,JVM 的热替换能力有所增强,支持了更多操作,但 IDEA 左侧 Debug 工具栏里依然只有Update按钮,其核心能力还是围绕方法体做文章。
3.2 Debug 模式下正确运用 Hot Swap
操作步骤非常简单:
第一步,用 Debug 模式启动 Spring Boot 应用,注意不是 Run,是 Debug。
第二步,修改代码。
第三步,IDEA 顶部工具栏里有个“Update”图标,或者直接按快捷键Ctrl+F10,选择Update classes,把修改后的 class 文件热替换到运行中的 JVM 里。
如果你开了Build project automatically,IDEA 会在类变更后自动编译,你只需要在修改完代码后切回 IDEA 窗口按Ctrl+F9,IDEA 会自动应用热替换,不需要手动点 Update。
这个方案在什么时候最香?我自己的高频场景是调整 SQL、修接口逻辑分支、加日志。比如一个OrderService里某个方法的计算逻辑写错了,直接进 Debug 模式,改一行代码,按Ctrl+F9,再看接口返回值,整个过程不到五秒,连请求都不用重发。
注意:Hot Swap 后,断点建议重新打一遍,IDEA 有时会对旧断点位置产生偏移,这个不算 bug,是字节码行号映射导致的正常现象。
3.3 什么时候用 Hot Swap 就够了?
很多开发者一听 Hot Swap 能力有限,就直接放弃,我觉得有点可惜。
我日常工作中有 60% 左右的代码改动,是修bug、调参数、改判断条件,这类改动落点都在方法体内部,Hot Swap 完全够用。如果你做了架构调整,需要新增一个服务类、加一个字段,那就不该用 Hot Swap 硬顶,应该交给 DevTools 重启,或者干脆用 JRebel。
Hot Swap 还有一个我特别喜欢的场景:联调接口。我们经常调第三方接口,联调时对方接口返回的数据格式有点问题,你只需要在 Debug 模式改掉对返回值的解析逻辑,热替换后继续调,不用重新发请求,体验非常顺滑。
需要提醒的是,Hot Swap 只对 JVM 内已经加载的类有效。如果你新增了一个类,然后想用Ctrl+F10让新类生效,是不行的,必须重启应用。同样,如果你改了 Spring 配置文件里的路由、数据库连接,Hot Swap 也管不了,这些只能通过重启或 JRebel 解决。
4. 方案三:JRebel——真正的热加载,花钱买体验
4.1 安装与启用 JRebel
JRebel 是目前业界公认最强的 JVM 热加载插件,它从一开始就是为“完全不用重启”设计的。它和 DevTools 的最大区别在于:JRebel 会拦截类的加载和调用,把新编译的类动态注入到运行中的 JVM 里,甚至支持新增类、新增字段、修改方法签名这类结构性变化。
安装很简单。IDEA 的插件市场直接搜JRebel,安装后重启 IDEA,然后需要一个许可证。JRebel 是商业软件,支持官方试用,你可以去官网申请 14 天的试用 license,也可以购买正版授权。
激活后,JRebel 会在工具栏上多一组按钮。要让它生效,需要做两件事:
第一步,在 IDEA 的 Run/Debug Configurations 里找到你的 Spring Boot 启动类,把运行器切换为JRebel Spring Boot。
第二步,确保 JRebel 面板里的Enable JRebel开关是打开状态,并且你的项目模块出现在 JRebel 管理的模块列表里。
JRebel 启动后,控制台会多一行JRebel: 2024.2.0之类的版本信息。如果看到这行,说明 JRebel 已经在你项目里生效了。
4.2 JRebel 能热到什么程度
我用了 JRebel 一年半,来聊聊它的能力边界。
最日常的体验是:你改了 Controller 里的一个接口路径,比如把/api/order改成/api/v2/order,保存代码,编译,JRebel 能直接让新路径生效,不需要重启。这在 DevTools 和 Hot Swap 下都是做不到的,因为接口路径属于方法注解信息,Hot Swap 不能改,DevTools 只能重启。
新增一个 Service 实现类、在已有的类里加一个私有方法、给 Bean 增加一个@Autowired字段,JRebel 都能热加载。我实测过最复杂的场景是给一个已有的@Service加一个@Cacheable注解,JRebel 也可以生效,只是偶尔需要在它弹出的对话框里点一下确认。
在编辑 MyBatis 的 Mapper XML 时,JRebel 也支持热加载,改完 SQL 文件,编译一下就能生效。但这里有个前提:你的项目不能用太老版本的 MyBatis-spring-boot-starter,因为版本过老时 Mapper 的 XML 索引在启动时就被缓存了,JRebel 无法拦截。
有一个限制要提醒:网络框架级别的改动,比如修改监听器、过滤器链、修改 Servlet 容器初始化逻辑,JRebel 也未必能全量支持。这类改动该重启还是重启,别硬刚。
4.3 JRebel 与 DevTools 的冲突与选择
我在早期使用 JRebel 时犯过一个错误:项目里同时保留了 DevTools 的依赖,导致启动后两个工具都在工作,类加载被反复替换,出现了各种诡异异常。后来才明白,DevTools 的 restart classloader 机制和 JRebel 的动态注入机制天生冲突,二者只能选一个。
如果你决定用 JRebel,建议直接在pom.xml里把 DevTools 的依赖注释掉,或者在 Maven 配置里排除它,不要让两个工具同时存在。
选 DevTools 还是选 JRebel,说到底是一个“免费但重启 vs 付费但更快”的选择。个人建议大家分阶段:刚起步的项目,团队规模小,用 DevTools + Hot Swap 组合已经能覆盖 80% 场景;项目进入高速迭代期,接口和业务逻辑频繁变动,JRebel 节省的时间会非常可观,尤其对大型单体项目,一天省下的重启时间足够把插件钱赚回来。
5. 三种方案怎么选?我的日常搭配参考
5.1 以“改动类型”为导向的选型
很多人问:“到底哪种方案最好?”我觉得该反过来问:“我现在改的代码属于哪种类型?”
如果你在改方法里的业务逻辑,比如加判断、修 SQL、调返回结构,Hot Swap 是最快速的,连 DevTools 都不需要。Debug 模式起来,改完按一下Ctrl+F9就完事了。
如果你在加新的类、新的接口、改配置类,或者调整 Spring 的 Bean 关系,Hot Swap 直接没法用,DevTools 重启可以,JRebel 也可以。二者选谁取决于你的重启成本和预算。项目启动在 10 秒以内,用 DevTools 完全够;启动超过 20 秒,且每天改动频繁,预算允许的情况下可以上 JRebel。
如果你是纯前端页面调整,比如改模板文件、样式、静态资源,DevTools 的排除配置已经帮你把重启避开了,直接刷新浏览器就够,别把后端热加载方案搅和进来。
5.2 我保留的一份高效开发环境清单
如果你不想踩坑,可以直接参考我这套配置组合:
- 项目里保留 DevTools 依赖,做好排除项和触发文件配置,作为兜底方案。
- 日常 Debug 模式启动应用,利用 Hot Swap 处理高频的“方法体改动”。
- 当需要加字段、加类这种结构性改动时,用
Ctrl+F9主动编译,让 DevTools 重启一次。 - 如果项目规模大、团队协作频繁,再给每个人都配上 JRebel,让它接管 DevTools 的工作。
这套组合的好处是,即使某天 JRebel 授权到期,你也不用改任何代码,DevTools 和 Hot Swap 立刻接管,开发节奏几乎不受影响。
6. 常见问题与排查技巧实录
6.1 DevTools 不生效,改完代码不重启
这个问题的位置非常高频,我几乎每周都能在同事的电脑上碰到一次。九成原因是:IDEA 没有把修改的内容编译到target/classes目录。
DevTools 监听的是类路径下文件的变化,而 IDEA 默认并不会每改一次代码就自动编译。你打开自动编译开关后,仍然有概率不生效,因为 IDEA 的自动编译并不是“实时”的,它有个延迟和触发机制。
排障顺序建议从简单到复杂:
- 检查
pom.xml里 devtools 是否还在,且没有被其他模块的依赖过滤掉。 - 手工按一次
Ctrl+F9,看控制台有没有Restarting日志。 - 查看右下角有没有“Compilation completed”提示,如果有编译异常,DevTools 不会重启。
- 如果以上都正常,考虑触发文件配置是否误加了
.trigger文件。
6.2 JRebel 与 Lombok 冲突导致热加载失效
JRebel 对 Lombok 有官方支持,但版本不匹配时会出现类加载失败。表现为:改了代码后,JRebel 日志报ClassNotFound或者某个方法找不到。
解决方法是统一 JRebel 与 Lombok 的版本。JRebel 官方维护了一个“兼容版本表”,建议把你的 Lombok 版本升级到较新的,比如 1.18.20 以上,同时 IDEA 的 Lombok 插件版本也一并更新。如果问题还在,试试在启动参数里加上:
-Dlombok.jrebel=true这个参数强制开启 JRebel 对 Lombok 生成代码的适配,实测能解决九成冲突。
6.3 热加载后 Session 丢失、连接池异常
热加载和重启都会重建 Spring 容器,这意味着:Session 里保存的登录态、内存缓存、@Scheduled定时器,都会在重启后重新初始化。DevTools 的“自动重启”每次都会重建容器,所以影响最明显。
解决思路有两个方向:第一,把关键状态外置。登录态放到 Redis Session、缓存放到 Redis,这样重启后数据还在,用户无需重新登录。第二,对重启频繁的开发环境,可以适当降低重启频率,把零散的改动攒成一次编译,用触发文件控制重启节奏。
连接池异常的典型表现是:重启后第一条 SQL 执行报“Connection is closed”,这是因为连接池重建前的旧连接没有正确释放。DevTools 的 restart classloader 会持有旧连接池对象的引用,导致新容器的数据库操作访问旧连接。比较有效的规避方案是给连接池配置较短的空闲回收时间,或者在开发环境把连接池的maximum-pool-size调小一点,降低连接泄露风险。
6.4 快速排查工具汇总
| 现象 | 可能原因 | 排查/解决路径 |
|---|---|---|
| 改了代码不重启 | IDEA 未触发编译 | 检查自动编译开关,手工Ctrl+F9 |
| 控制台没有 LiveReload 日志 | devtools 被可选依赖排除 | 检查 pom 中optional标签,确认依赖存在 |
| Hot Swap 后断点灰掉 | 方法行号映射变化 | 停掉断点重新打一次 |
| JRebel 报了 RedefineClassException | 改动触及类结构 | 该场景需重启应用,建议看控制台输出 |
| 重启后 Session 丢失 | 容器重建 | 使用 Redis Session 或减少重启频率 |
| 改模板不生效 | 模板缓存开启 | 设置spring.thymeleaf.cache=false |
最后再分享一个实际经验
我踩过不少热加载的坑之后,现在的习惯是:白天大块时间开发时用 DevTools + Hot Swap,Debug 模式一直开着,改方法体靠热替换,改结构靠 DevTools 重启;遇到大型重构或者多模块联调时,再打开 JRebel。这套组合用下来,平均每天能省下至少半小时的等待时间,长期积累非常可观。
还有个小技巧分享一下:如果你和同事在同一个模块上协同开发,建议把 DevTools 的触发文件.trigger纳入 git 忽略,不然每次拉取代码都可能导致不必要的重启。另外,热加载方案只是工具,真正重要的还是把模块拆得合理,启动速度本来就会更快。工具选对,思路清晰,加班自然就少了。别把时间耗在等待上,有时间多看看日志、多想想系统设计,那才是程序员最值钱的部分。