优先选ApplicationRunner。它直接解析带破折号的启动参数(如--cache.warmup=true),比CommandLineRunner更健壮;需显式@Order控制顺序;耗时预热应异步执行并加超时;多实例需分布式锁或Leader选举避免重复;异常必须主动捕获并log.error,否则静默失败。

缓存预热该用 CommandLineRunner 还是 ApplicationRunner?
优先选 ApplicationRunner。它接收封装好的 ApplicationArguments,能直接解析带破折号的参数(比如 --cache.warmup=true),而 CommandLineRunner 拿到的是原始字符串数组,手动解析容易出错、不健壮。
常见错误现象:用 CommandLineRunner 试图读取 --spring.profiles.active=dev 这类参数,结果要自己切分、判断前缀、处理引号,一升级 Spring Boot 版本就崩。
-
ApplicationRunner的run()方法参数类型更语义化,适合配置驱动型预热逻辑 - 如果项目完全不依赖启动参数,二者行为一致,但别为“简单”牺牲可维护性
- 多个实现类时,都必须显式设置
@Order,否则执行顺序不确定——Spring 不保证加载顺序
预热逻辑卡住启动怎么办?
默认所有 Runner 在主线程同步执行,一旦缓存加载慢(比如查大表、调外部 HTTP 接口),整个应用就 hang 在 STARTING 状态,健康检查失败,K8s 可能直接 kill。
正确做法是把耗时操作移出 run(),改用异步触发 + 启动后监听:
立即学习“Java免费学习笔记(深入)”;
- 在
run()里只提交任务:taskExecutor.submit(this::doWarmup) - 不要用
CompletableFuture.runAsync()默认线程池——它可能被其他模块共用并压垮 - 给预热任务加超时控制,例如用
ScheduledExecutorService配合Future.get(30, TimeUnit.SECONDS),超时则记录告警但不阻塞启动 - 避免在预热中初始化未就绪的 Bean(如尚未完成事务代理的 Service),会触发提前初始化异常
BeanCurrentlyInCreationException
多实例部署时重复预热怎么破?
集群里每个节点都执行一遍缓存预热,不仅浪费资源,还可能引发数据覆盖或并发写冲突(比如预热时往 Redis 写全量商品列表)。
本质不是 Runner 的问题,而是缺乏分布式协调。可行解法只有两个:
- 用中心化开关:启动时先查数据库/配置中心某个
warmup.lockkey,存在则跳过;不存在则SETNX占坑,成功者执行,失败者等待重试或直接退出 - 依赖注册中心:仅让 Leader 实例执行(如 Nacos 的
LeaderElection或 Eureka 中选取 IP 最小的服务实例) - 绝对不要用本地内存标志位(如
static boolean warmed = false)——对多进程无效
预热失败了,日志里却没报错?
因为 Runner 的异常默认被 Spring 吞掉,只打 DEBUG 日志,INFO 级别完全看不到。线上环境通常关 DEBUG,结果就是“什么都没发生,但缓存就是空的”。
必须主动捕获并提升日志级别:
- 所有预热代码包在
try-catch里,catch (Exception e)后用log.error("Cache warmup failed", e) - 别依赖
e.printStackTrace()——它输出到 stdout,K8s 日志收集器很可能漏掉 - 在 catch 块末尾抛出
RuntimeException,强制让 Spring 启动失败(适用于强依赖缓存的场景) - 检查是否用了 Lombok 的
@SneakyThrows——它会静默吞掉 checked exception,让问题更隐蔽
预热不是“启动时跑一段代码”那么简单,它连着配置解析、线程模型、分布式协同和错误可见性四根线,断一根,线上就少一半缓存命中率。










