本文深入讲解 Spring Boot 的 CommandLineRunner 接口:从源码、基础使用、执行顺序到缓存预热、数据库初始化等实战场景,以及常见踩坑与最佳实践,帮你彻底掌握启动回调机制。
CommandLineRunner接口
- Hook
- 接口定义(源码速览)
- 基础使用
-
- [方式一:实现 CommandLineRunner 接口](#方式一:实现 CommandLineRunner 接口)
- [方式二:@Bean + Lambda(推荐)](#方式二:@Bean + Lambda(推荐))
- [CommandLineRunner vs ApplicationRunner ⭐](#CommandLineRunner vs ApplicationRunner ⭐)
- [多个 Runner 的执行顺序](#多个 Runner 的执行顺序)
- 实战场景
-
- [场景一:数据库初始化(检测 + 建表 + 插数据)](#场景一:数据库初始化(检测 + 建表 + 插数据))
- 场景二:缓存预热
- 场景三:启动健康检查(依赖服务探测)
- [场景四:Elasticsearch 索引初始化](#场景四:Elasticsearch 索引初始化)
- [源码解析:Spring Boot 如何调用 Runner](#源码解析:Spring Boot 如何调用 Runner)
- [与同类回调的对比 ⭐](#与同类回调的对比 ⭐)
- [常见踩坑与最佳实践 ⚠️](#常见踩坑与最佳实践 ⚠️)
-
- [坑一:Runner 中抛异常导致应用启动失败](#坑一:Runner 中抛异常导致应用启动失败)
- [坑二:Runner 中执行太久,阻塞启动](#坑二:Runner 中执行太久,阻塞启动)
- [坑三:@Order 没生效](#坑三:@Order 没生效)
- [坑四:在 Runner 中做 RPC 调用](#坑四:在 Runner 中做 RPC 调用)
- 最佳实践清单
- 总结速查卡
Hook
凌晨两点,运维群里炸了:新上的服务刚启动,前 100 个请求全部超时。排查发现Redis 缓存是空的,所有请求直接穿透到数据库,瞬间打满连接池。加几行代码,让应用在接收流量之前就把缓存预热好 ,这就是 CommandLineRunner 最典型的应用场景。
CommandLineRunner 是 Spring Boot 提供的一个回调接口 ,它会在 Spring 容器初始化完成后、应用开始对外服务之前执行。可以把它理解为 Spring Boot 启动流程中的最后一道关卡------在这里做预热、初始化、校验,确保应用万事俱备之后再接流量。
应用就绪 CommandLineRunner Bean 初始化 ApplicationContext SpringApplication main() 应用就绪 CommandLineRunner Bean 初始化 ApplicationContext SpringApplication main() #mermaid-svg-Ctck9bGtc0KRFQ3x{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ctck9bGtc0KRFQ3x .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ctck9bGtc0KRFQ3x .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ctck9bGtc0KRFQ3x .error-icon{fill:#552222;}#mermaid-svg-Ctck9bGtc0KRFQ3x .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ctck9bGtc0KRFQ3x .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ctck9bGtc0KRFQ3x .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ctck9bGtc0KRFQ3x .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ctck9bGtc0KRFQ3x .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ctck9bGtc0KRFQ3x .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ctck9bGtc0KRFQ3x .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ctck9bGtc0KRFQ3x .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ctck9bGtc0KRFQ3x .marker.cross{stroke:#333333;}#mermaid-svg-Ctck9bGtc0KRFQ3x svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ctck9bGtc0KRFQ3x p{margin:0;}#mermaid-svg-Ctck9bGtc0KRFQ3x .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Ctck9bGtc0KRFQ3x text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Ctck9bGtc0KRFQ3x .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Ctck9bGtc0KRFQ3x .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Ctck9bGtc0KRFQ3x .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Ctck9bGtc0KRFQ3x .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Ctck9bGtc0KRFQ3x #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Ctck9bGtc0KRFQ3x .sequenceNumber{fill:white;}#mermaid-svg-Ctck9bGtc0KRFQ3x #sequencenumber{fill:#333;}#mermaid-svg-Ctck9bGtc0KRFQ3x #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Ctck9bGtc0KRFQ3x .messageText{fill:#333;stroke:none;}#mermaid-svg-Ctck9bGtc0KRFQ3x .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Ctck9bGtc0KRFQ3x .labelText,#mermaid-svg-Ctck9bGtc0KRFQ3x .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Ctck9bGtc0KRFQ3x .loopText,#mermaid-svg-Ctck9bGtc0KRFQ3x .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Ctck9bGtc0KRFQ3x .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Ctck9bGtc0KRFQ3x .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Ctck9bGtc0KRFQ3x .noteText,#mermaid-svg-Ctck9bGtc0KRFQ3x .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Ctck9bGtc0KRFQ3x .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Ctck9bGtc0KRFQ3x .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Ctck9bGtc0KRFQ3x .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Ctck9bGtc0KRFQ3x .actorPopupMenu{position:absolute;}#mermaid-svg-Ctck9bGtc0KRFQ3x .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Ctck9bGtc0KRFQ3x .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Ctck9bGtc0KRFQ3x .actor-man circle,#mermaid-svg-Ctck9bGtc0KRFQ3x line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Ctck9bGtc0KRFQ3x :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 环境准备、Profile 加载 这里做缓存预热、数据初始化 健康检查、配置校验... 1. SpringApplication.run() 2. 创建并刷新 ApplicationContext 3. 扫描、实例化、注入 Bean 4. @PostConstruct 回调 5. Bean 初始化完成 6. ⭐ 回调所有 CommandLineRunner 7. 所有 Runner 执行完毕 8. 应用就绪,开始接收请求 🚀
接口定义(源码速览)
CommandLineRunner 的源码非常精简,就一个方法:
java
@FunctionalInterface
public interface CommandLineRunner {
/**
* Callback used to run the bean.
* @param args incoming main method arguments
*/
void run(String... args) throws Exception;
}
@FunctionalInterface 注解意味着:它可以用 Lambda 表达式实现(后面会演示)。
run(String... args) 中的 args 就是 main(String[] args) 传入的启动参数。比如:
bash
java -jar app.jar --server.port=8080 --env=prod
# └─── 这两个也是 args ──┘
但实际上 args 里面装的是未被 Spring 解析的原始参数 --------server.port 这种标准 Spring 参数已经被 SpringApplication 提前消费掉了,run() 里拿到的主要是你自定义的非标准参数。
基础使用
方式一:实现 CommandLineRunner 接口
最传统的写法------让某个 Bean 直接实现接口:
java
@Slf4j
@Component
public class DataInitRunner implements CommandLineRunner {
@Autowired
private UserService userService;
@Override
public void run(String... args) throws Exception {
log.info("===== 开始数据初始化 =====");
// 检测管理员账号是否存在,不存在则创建
if (!userService.existsAdmin()) {
userService.createDefaultAdmin();
log.info("默认管理员账号已创建");
}
log.info("===== 数据初始化完成 =====");
}
}
方式二:@Bean + Lambda(推荐)
如果逻辑比较简单,直接在配置类中用 Lambda 注入,代码更紧凑:
java
@Configuration
public class RunnerConfig {
@Bean
public CommandLineRunner cacheWarmUp(CacheService cacheService) {
return args -> {
log.info("===== 开始缓存预热 =====");
cacheService.preloadHotData();
log.info("===== 缓存预热完成 =====");
};
}
}
什么时候用
@BeanLambda 写法? 逻辑简单、不跨多个 Service、不需要复用时。什么时候实现接口? 逻辑复杂、需要注入多个依赖、有完整的类结构更清晰时。
CommandLineRunner vs ApplicationRunner ⭐
Spring Boot 提供了两个 功能几乎一样的启动回调接口,它们的唯一区别是参数类型:
| 对比维度 | CommandLineRunner | ApplicationRunner |
|---|---|---|
| 方法签名 | run(String... args) |
run(ApplicationArguments args) |
| 参数类型 | 原始字符串数组 | 封装对象 |
| 获取选项参数 | 手动解析 | args.getOptionValues("key") |
| 获取非选项参数 | 手动解析 | args.getNonOptionArgs() |
| 判断参数是否存在 | 遍历数组 | args.containsOption("key") |
| 适用场景 | 简单启动逻辑 | 需要解析复杂启动参数 |
ApplicationRunner 用法示例:
java
@Component
public class ProfileAwareRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 判断启动参数中是否有 --env=prod
if (args.containsOption("env")) {
List<String> envValues = args.getOptionValues("env");
log.info("启动环境:{}", envValues.get(0));
}
// 获取非选项参数(如 java -jar app.jar file1.txt file2.txt)
List<String> nonOptionArgs = args.getNonOptionArgs();
log.info("非选项参数:{}", nonOptionArgs);
}
}
选型建议:
#mermaid-svg-PyWv7bDNFFjitpT9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PyWv7bDNFFjitpT9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PyWv7bDNFFjitpT9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PyWv7bDNFFjitpT9 .error-icon{fill:#552222;}#mermaid-svg-PyWv7bDNFFjitpT9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PyWv7bDNFFjitpT9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PyWv7bDNFFjitpT9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PyWv7bDNFFjitpT9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PyWv7bDNFFjitpT9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PyWv7bDNFFjitpT9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PyWv7bDNFFjitpT9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PyWv7bDNFFjitpT9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PyWv7bDNFFjitpT9 .marker.cross{stroke:#333333;}#mermaid-svg-PyWv7bDNFFjitpT9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PyWv7bDNFFjitpT9 p{margin:0;}#mermaid-svg-PyWv7bDNFFjitpT9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PyWv7bDNFFjitpT9 .cluster-label text{fill:#333;}#mermaid-svg-PyWv7bDNFFjitpT9 .cluster-label span{color:#333;}#mermaid-svg-PyWv7bDNFFjitpT9 .cluster-label span p{background-color:transparent;}#mermaid-svg-PyWv7bDNFFjitpT9 .label text,#mermaid-svg-PyWv7bDNFFjitpT9 span{fill:#333;color:#333;}#mermaid-svg-PyWv7bDNFFjitpT9 .node rect,#mermaid-svg-PyWv7bDNFFjitpT9 .node circle,#mermaid-svg-PyWv7bDNFFjitpT9 .node ellipse,#mermaid-svg-PyWv7bDNFFjitpT9 .node polygon,#mermaid-svg-PyWv7bDNFFjitpT9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PyWv7bDNFFjitpT9 .rough-node .label text,#mermaid-svg-PyWv7bDNFFjitpT9 .node .label text,#mermaid-svg-PyWv7bDNFFjitpT9 .image-shape .label,#mermaid-svg-PyWv7bDNFFjitpT9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PyWv7bDNFFjitpT9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PyWv7bDNFFjitpT9 .rough-node .label,#mermaid-svg-PyWv7bDNFFjitpT9 .node .label,#mermaid-svg-PyWv7bDNFFjitpT9 .image-shape .label,#mermaid-svg-PyWv7bDNFFjitpT9 .icon-shape .label{text-align:center;}#mermaid-svg-PyWv7bDNFFjitpT9 .node.clickable{cursor:pointer;}#mermaid-svg-PyWv7bDNFFjitpT9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PyWv7bDNFFjitpT9 .arrowheadPath{fill:#333333;}#mermaid-svg-PyWv7bDNFFjitpT9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PyWv7bDNFFjitpT9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PyWv7bDNFFjitpT9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PyWv7bDNFFjitpT9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PyWv7bDNFFjitpT9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PyWv7bDNFFjitpT9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PyWv7bDNFFjitpT9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PyWv7bDNFFjitpT9 .cluster text{fill:#333;}#mermaid-svg-PyWv7bDNFFjitpT9 .cluster span{color:#333;}#mermaid-svg-PyWv7bDNFFjitpT9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PyWv7bDNFFjitpT9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PyWv7bDNFFjitpT9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PyWv7bDNFFjitpT9 .icon-shape,#mermaid-svg-PyWv7bDNFFjitpT9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PyWv7bDNFFjitpT9 .icon-shape p,#mermaid-svg-PyWv7bDNFFjitpT9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PyWv7bDNFFjitpT9 .icon-shape .label rect,#mermaid-svg-PyWv7bDNFFjitpT9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PyWv7bDNFFjitpT9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PyWv7bDNFFjitpT9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PyWv7bDNFFjitpT9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
需要解析 --key=value
格式的启动参数?
逻辑非常简单?
用 ApplicationRunner
getOptionValues() 一行搞定
用 CommandLineRunner
Lambda 写法最简洁
两个都行,看团队规范
CommandLineRunner 更常见
选择启动回调
实际项目中 90% 的场景用
CommandLineRunner就够了,除非你需要解析复杂的命令行参数结构。
多个 Runner 的执行顺序
当定义了多个 CommandLineRunner(或混用了 ApplicationRunner)时,用 @Order 控制执行顺序。
java
@Slf4j
@Component
@Order(1)
public class ConfigCheckRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.info("[Order=1] 第一步:配置校验...");
}
}
@Slf4j
@Component
@Order(2)
public class DataInitRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.info("[Order=2] 第二步:数据初始化...");
}
}
@Slf4j
@Component
@Order(3)
public class CacheWarmUpRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.info("[Order=3] 第三步:缓存预热...");
}
}
执行输出:
[Order=1] 第一步:配置校验...
[Order=2] 第二步:数据初始化...
[Order=3] 第三步:缓存预热...
#mermaid-svg-pYmLSvMZWwh4iAm1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pYmLSvMZWwh4iAm1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pYmLSvMZWwh4iAm1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pYmLSvMZWwh4iAm1 .error-icon{fill:#552222;}#mermaid-svg-pYmLSvMZWwh4iAm1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pYmLSvMZWwh4iAm1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pYmLSvMZWwh4iAm1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pYmLSvMZWwh4iAm1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pYmLSvMZWwh4iAm1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pYmLSvMZWwh4iAm1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pYmLSvMZWwh4iAm1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pYmLSvMZWwh4iAm1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pYmLSvMZWwh4iAm1 .marker.cross{stroke:#333333;}#mermaid-svg-pYmLSvMZWwh4iAm1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pYmLSvMZWwh4iAm1 p{margin:0;}#mermaid-svg-pYmLSvMZWwh4iAm1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pYmLSvMZWwh4iAm1 .cluster-label text{fill:#333;}#mermaid-svg-pYmLSvMZWwh4iAm1 .cluster-label span{color:#333;}#mermaid-svg-pYmLSvMZWwh4iAm1 .cluster-label span p{background-color:transparent;}#mermaid-svg-pYmLSvMZWwh4iAm1 .label text,#mermaid-svg-pYmLSvMZWwh4iAm1 span{fill:#333;color:#333;}#mermaid-svg-pYmLSvMZWwh4iAm1 .node rect,#mermaid-svg-pYmLSvMZWwh4iAm1 .node circle,#mermaid-svg-pYmLSvMZWwh4iAm1 .node ellipse,#mermaid-svg-pYmLSvMZWwh4iAm1 .node polygon,#mermaid-svg-pYmLSvMZWwh4iAm1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pYmLSvMZWwh4iAm1 .rough-node .label text,#mermaid-svg-pYmLSvMZWwh4iAm1 .node .label text,#mermaid-svg-pYmLSvMZWwh4iAm1 .image-shape .label,#mermaid-svg-pYmLSvMZWwh4iAm1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-pYmLSvMZWwh4iAm1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pYmLSvMZWwh4iAm1 .rough-node .label,#mermaid-svg-pYmLSvMZWwh4iAm1 .node .label,#mermaid-svg-pYmLSvMZWwh4iAm1 .image-shape .label,#mermaid-svg-pYmLSvMZWwh4iAm1 .icon-shape .label{text-align:center;}#mermaid-svg-pYmLSvMZWwh4iAm1 .node.clickable{cursor:pointer;}#mermaid-svg-pYmLSvMZWwh4iAm1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pYmLSvMZWwh4iAm1 .arrowheadPath{fill:#333333;}#mermaid-svg-pYmLSvMZWwh4iAm1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pYmLSvMZWwh4iAm1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pYmLSvMZWwh4iAm1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pYmLSvMZWwh4iAm1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pYmLSvMZWwh4iAm1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pYmLSvMZWwh4iAm1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pYmLSvMZWwh4iAm1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pYmLSvMZWwh4iAm1 .cluster text{fill:#333;}#mermaid-svg-pYmLSvMZWwh4iAm1 .cluster span{color:#333;}#mermaid-svg-pYmLSvMZWwh4iAm1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pYmLSvMZWwh4iAm1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pYmLSvMZWwh4iAm1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-pYmLSvMZWwh4iAm1 .icon-shape,#mermaid-svg-pYmLSvMZWwh4iAm1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pYmLSvMZWwh4iAm1 .icon-shape p,#mermaid-svg-pYmLSvMZWwh4iAm1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pYmLSvMZWwh4iAm1 .icon-shape .label rect,#mermaid-svg-pYmLSvMZWwh4iAm1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pYmLSvMZWwh4iAm1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pYmLSvMZWwh4iAm1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pYmLSvMZWwh4iAm1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 启动流程
Spring 容器
初始化完成
@Order(1)
ConfigCheckRunner
@Order(2)
DataInitRunner
@Order(3)
CacheWarmUpRunner
应用就绪 ✅
执行顺序规则:
| 情况 | 规则 |
|---|---|
都有 @Order |
数字越小越先执行 (与切面 @Order 一致) |
部分有 @Order |
有 @Order 的先执行,Ordered.LOWEST_PRECEDENCE 作为默认值 |
都没有 @Order |
不保证顺序(依赖 Spring 扫描 Bean 的顺序,不可靠) |
混用 CommandLineRunner 和 ApplicationRunner |
按 Order 值统一排序,不区分接口类型 |
⚠️ 一定加
@Order!不要依赖"碰巧对"的默认顺序,加了才明确。
实战场景
场景一:数据库初始化(检测 + 建表 + 插数据)
服务首次部署时,数据库中没有任何数据,需要在启动时自动创建管理员账号和基础字典数据:
java
@Slf4j
@Component
@Order(1)
public class DatabaseInitRunner implements CommandLineRunner {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void run(String... args) throws Exception {
log.info("===== 检查数据库初始化状态 =====");
// 1. 检查 admin 用户是否存在
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM sys_user WHERE username = 'admin'",
Integer.class);
if (count != null && count == 0) {
log.info("管理员账号不存在,开始创建...");
// 2. 插入默认角色
jdbcTemplate.update(
"INSERT INTO sys_role (role_name, role_code) VALUES (?, ?)",
"超级管理员", "ROLE_ADMIN");
// 3. 插入管理员账号
jdbcTemplate.update(
"INSERT INTO sys_user (username, password, nickname, role_id) VALUES (?, ?, ?, ?)",
"admin",
passwordEncoder.encode("admin123"),
"系统管理员",
1);
log.info("✅ 默认管理员账号已创建(用户名: admin / 密码: admin123)");
} else {
log.info("管理员账号已存在,跳过初始化");
}
// 4. 初始化字典数据(同样检测是否存在)
initDictData();
log.info("===== 数据库初始化完成 =====");
}
private void initDictData() {
// 初始化性别、状态等基础字典
Integer dictCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM sys_dict WHERE dict_type = 'sys_user_status'",
Integer.class);
if (dictCount != null && dictCount == 0) {
jdbcTemplate.update(
"INSERT INTO sys_dict (dict_type, dict_label, dict_value) VALUES " +
"('sys_user_status', '正常', '1'), " +
"('sys_user_status', '停用', '0')");
log.info("基础字典数据已初始化");
}
}
}
场景二:缓存预热
服务刚启动时 Redis 是空的,前几百个请求全部穿透到数据库,容易引发雪崩。在 CommandLineRunner 中把热点数据预先加载到 Redis:
java
@Slf4j
@Component
@Order(2) // ← 在数据库初始化之后执行
public class CacheWarmUpRunner implements CommandLineRunner {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductService productService;
@Autowired
private DictService dictService;
// 预热数据量阈值:超过这个量走异步
private static final int SYNC_WARM_UP_LIMIT = 10000;
@Override
public void run(String... args) throws Exception {
long start = System.currentTimeMillis();
log.info("===== 开始缓存预热 =====");
// 1. 预热字典数据(数量少,同步加载)
warmUpDictCache();
// 2. 预热热点商品(数量多,异步加载)
// 用异步避免阻塞启动流程
CompletableFuture.runAsync(() -> warmUpHotProducts());
log.info("===== 缓存预热触发完成,耗时 {}ms =====",
System.currentTimeMillis() - start);
}
private void warmUpDictCache() {
List<Dict> dictList = dictService.listAll();
for (Dict dict : dictList) {
String key = "dict:" + dict.getDictType();
redisTemplate.opsForValue().set(key, dict, 1, TimeUnit.HOURS);
}
log.info("字典缓存预热完成,共 {} 条", dictList.size());
}
private void warmUpHotProducts() {
// 查询最近 7 天销量 Top 1000 的商品
List<Product> hotProducts = productService.getHotProducts(7, 1000);
for (Product product : hotProducts) {
String key = "product:detail:" + product.getId();
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
}
log.info("热点商品缓存预热完成,共 {} 条", hotProducts.size());
}
}
预热策略选择:少量核心数据(如字典)→ 同步加载,确保启动后立即可用;大量热点数据(如商品)→ 异步加载,不阻塞启动过程。
场景三:启动健康检查(依赖服务探测)
应用依赖了外部服务(数据库、Redis、Elasticsearch、MQ),启动时先检查这些服务是否连通,不通就直接终止启动,避免带着残血状态接流量:
java
@Slf4j
@Component
@Order(0) // ← 最高优先级,在任何其他 Runner 之前执行
public class HealthCheckRunner implements CommandLineRunner {
@Autowired
private DataSource dataSource;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void run(String... args) throws Exception {
log.info("===== 启动健康检查 =====");
boolean allHealthy = true;
// 1. 检查数据库连通性
try (Connection conn = dataSource.getConnection()) {
if (conn.isValid(5)) {
log.info("✅ 数据库连通正常");
} else {
log.error("❌ 数据库连通失败");
allHealthy = false;
}
} catch (Exception e) {
log.error("❌ 数据库连接异常:{}", e.getMessage());
allHealthy = false;
}
// 2. 检查 Redis 连通性
try {
String pong = Objects.requireNonNull(
redisTemplate.getConnectionFactory())
.getConnection().ping();
if ("PONG".equals(pong)) {
log.info("✅ Redis 连通正常");
} else {
log.error("❌ Redis 连通异常,返回:{}", pong);
allHealthy = false;
}
} catch (Exception e) {
log.error("❌ Redis 连接异常:{}", e.getMessage());
allHealthy = false;
}
// 3. 汇总结果
if (!allHealthy) {
// 👇 关键:直接抛异常,阻止应用启动
throw new IllegalStateException(
"启动健康检查失败,部分依赖服务不可用,应用终止启动");
}
log.info("===== 启动健康检查全部通过 ✅ =====");
}
}
效果: 如果 Redis 没启动,应用会在启动阶段直接报错退出,而不是带着"半残"状态启动后在运行时不断抛异常。
Redis 数据库 HealthCheckRunner 应用启动 Redis 数据库 HealthCheckRunner 应用启动 #mermaid-svg-sr0Sm2ktDviIqrPp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sr0Sm2ktDviIqrPp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sr0Sm2ktDviIqrPp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sr0Sm2ktDviIqrPp .error-icon{fill:#552222;}#mermaid-svg-sr0Sm2ktDviIqrPp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sr0Sm2ktDviIqrPp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sr0Sm2ktDviIqrPp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sr0Sm2ktDviIqrPp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sr0Sm2ktDviIqrPp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sr0Sm2ktDviIqrPp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sr0Sm2ktDviIqrPp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sr0Sm2ktDviIqrPp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sr0Sm2ktDviIqrPp .marker.cross{stroke:#333333;}#mermaid-svg-sr0Sm2ktDviIqrPp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sr0Sm2ktDviIqrPp p{margin:0;}#mermaid-svg-sr0Sm2ktDviIqrPp .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sr0Sm2ktDviIqrPp text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-sr0Sm2ktDviIqrPp .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sr0Sm2ktDviIqrPp .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-sr0Sm2ktDviIqrPp .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-sr0Sm2ktDviIqrPp .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-sr0Sm2ktDviIqrPp #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-sr0Sm2ktDviIqrPp .sequenceNumber{fill:white;}#mermaid-svg-sr0Sm2ktDviIqrPp #sequencenumber{fill:#333;}#mermaid-svg-sr0Sm2ktDviIqrPp #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-sr0Sm2ktDviIqrPp .messageText{fill:#333;stroke:none;}#mermaid-svg-sr0Sm2ktDviIqrPp .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sr0Sm2ktDviIqrPp .labelText,#mermaid-svg-sr0Sm2ktDviIqrPp .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-sr0Sm2ktDviIqrPp .loopText,#mermaid-svg-sr0Sm2ktDviIqrPp .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-sr0Sm2ktDviIqrPp .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sr0Sm2ktDviIqrPp .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-sr0Sm2ktDviIqrPp .noteText,#mermaid-svg-sr0Sm2ktDviIqrPp .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-sr0Sm2ktDviIqrPp .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sr0Sm2ktDviIqrPp .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sr0Sm2ktDviIqrPp .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sr0Sm2ktDviIqrPp .actorPopupMenu{position:absolute;}#mermaid-svg-sr0Sm2ktDviIqrPp .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-sr0Sm2ktDviIqrPp .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sr0Sm2ktDviIqrPp .actor-man circle,#mermaid-svg-sr0Sm2ktDviIqrPp line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-sr0Sm2ktDviIqrPp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 应用启动失败,进程退出 K8s 会自动重启 Pod 应用正常就绪 ✅ alt 健康检查失败 全部通过 1. 执行健康检查 2. 尝试获取连接 ✅ 连通 3. PING ❌ 连接超时 4. throw IllegalStateException 4. 继续启动
场景四:Elasticsearch 索引初始化
使用 ES 的项目,索引不存在时直接写入会报错。在启动时检查并创建索引和映射:
java
@Slf4j
@Component
@Order(3)
public class ElasticsearchInitRunner implements CommandLineRunner {
@Autowired
private RestHighLevelClient esClient;
private static final String INDEX_NAME = "product_index";
private static final int SHARDS = 3;
private static final int REPLICAS = 1;
@Override
public void run(String... args) throws Exception {
log.info("===== 检查 ES 索引 =====");
// 获取索引客户端
IndicesClient indicesClient = esClient.indices();
// 检查索引是否存在
GetIndexRequest getRequest = new GetIndexRequest(INDEX_NAME);
boolean exists = indicesClient.exists(getRequest, RequestOptions.DEFAULT);
if (!exists) {
log.info("索引 [{}] 不存在,开始创建...", INDEX_NAME);
CreateIndexRequest createRequest = new CreateIndexRequest(INDEX_NAME);
createRequest.settings(Settings.builder()
.put("index.number_of_shards", SHARDS)
.put("index.number_of_replicas", REPLICAS)
.put("index.refresh_interval", "30s"));
// 定义 mapping
createRequest.mapping(
"{\n" +
" \"properties\": {\n" +
" \"id\": { \"type\": \"long\" },\n" +
" \"name\": { \"type\": \"text\", \"analyzer\": \"ik_max_word\" },\n" +
" \"price\": { \"type\": \"double\" },\n" +
" \"createTime\": { \"type\": \"date\" }\n" +
" }\n" +
"}",
XContentType.JSON);
CreateIndexResponse response = indicesClient.create(
createRequest, RequestOptions.DEFAULT);
if (response.isAcknowledged()) {
log.info("✅ 索引 [{}] 创建成功", INDEX_NAME);
} else {
log.error("❌ 索引 [{}] 创建失败", INDEX_NAME);
throw new RuntimeException("ES 索引创建失败");
}
} else {
log.info("索引 [{}] 已存在,跳过创建", INDEX_NAME);
}
log.info("===== ES 索引检查完成 =====");
}
}
源码解析:Spring Boot 如何调用 Runner
好奇心重的话,来看看 Spring Boot 在源码层面是怎么调用这些 Runner 的。
你的 CommandLineRunner callRunners() afterRefresh() refreshContext() SpringApplication.run() SpringApplication main(String... args) 你的 CommandLineRunner callRunners() afterRefresh() refreshContext() SpringApplication.run() SpringApplication main(String... args) #mermaid-svg-DxZbKPbWdpGlLMdT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DxZbKPbWdpGlLMdT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DxZbKPbWdpGlLMdT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DxZbKPbWdpGlLMdT .error-icon{fill:#552222;}#mermaid-svg-DxZbKPbWdpGlLMdT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DxZbKPbWdpGlLMdT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DxZbKPbWdpGlLMdT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DxZbKPbWdpGlLMdT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DxZbKPbWdpGlLMdT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DxZbKPbWdpGlLMdT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DxZbKPbWdpGlLMdT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DxZbKPbWdpGlLMdT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DxZbKPbWdpGlLMdT .marker.cross{stroke:#333333;}#mermaid-svg-DxZbKPbWdpGlLMdT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DxZbKPbWdpGlLMdT p{margin:0;}#mermaid-svg-DxZbKPbWdpGlLMdT .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DxZbKPbWdpGlLMdT text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-DxZbKPbWdpGlLMdT .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DxZbKPbWdpGlLMdT .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-DxZbKPbWdpGlLMdT .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-DxZbKPbWdpGlLMdT .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-DxZbKPbWdpGlLMdT #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-DxZbKPbWdpGlLMdT .sequenceNumber{fill:white;}#mermaid-svg-DxZbKPbWdpGlLMdT #sequencenumber{fill:#333;}#mermaid-svg-DxZbKPbWdpGlLMdT #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-DxZbKPbWdpGlLMdT .messageText{fill:#333;stroke:none;}#mermaid-svg-DxZbKPbWdpGlLMdT .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DxZbKPbWdpGlLMdT .labelText,#mermaid-svg-DxZbKPbWdpGlLMdT .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-DxZbKPbWdpGlLMdT .loopText,#mermaid-svg-DxZbKPbWdpGlLMdT .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-DxZbKPbWdpGlLMdT .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DxZbKPbWdpGlLMdT .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-DxZbKPbWdpGlLMdT .noteText,#mermaid-svg-DxZbKPbWdpGlLMdT .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-DxZbKPbWdpGlLMdT .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DxZbKPbWdpGlLMdT .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DxZbKPbWdpGlLMdT .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DxZbKPbWdpGlLMdT .actorPopupMenu{position:absolute;}#mermaid-svg-DxZbKPbWdpGlLMdT .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-DxZbKPbWdpGlLMdT .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DxZbKPbWdpGlLMdT .actor-man circle,#mermaid-svg-DxZbKPbWdpGlLMdT line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-DxZbKPbWdpGlLMdT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 扫描 Bean、依赖注入 @PostConstruct 等 从容器中获取所有 CommandLineRunner 和 ApplicationRunner loop 按顺序执行每一个 Runner 应用就绪,开始接收请求 🚀 1. SpringApplication.run(args) 2. run(args) 3. refreshContext(context) 4. 容器刷新完成 5. afterRefresh(context, args) 6. callRunners(context, args) 7. 按 @Order 排序 8. runner.run(args) 9. 执行完成 10. 所有 Runner 执行完毕 11. 发布 ApplicationReadyEvent
核心源码:SpringApplication.run() → callRunners()
java
// SpringApplication.java(简化版本)
public ConfigurableApplicationContext run(String... args) {
// ... 前置准备 ...
try {
// ==== 关键步骤 ====
refreshContext(context); // ① 刷新容器(加载 Bean)
afterRefresh(context, args); // ② 这里调用 callRunners()
// ...
}
// ...异常处理...
}
// callRunners() 核心逻辑
private void callRunners(ApplicationContext context, ApplicationArguments args) {
// 从容器中获取所有 ApplicationRunner 和 CommandLineRunner
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
// 按 @Order 排序(数字小的在前)
AnnotationAwareOrderComparator.sort(runners);
// 逐个执行
for (Object runner : runners) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
关键发现 :
ApplicationRunner和CommandLineRunner是混在一起排序的,不区分类型 。所以如果你有@Order(1)的CommandLineRunner和@Order(1)的ApplicationRunner,它们会被放在同一优先级------执行顺序取决于AnnotationAwareOrderComparator的稳定排序结果。
与同类回调的对比 ⭐
Spring 生态里能在"启动时"做事情的机制不止 CommandLineRunner 一种。搞清楚各自的位置和适用场景:
ApplicationReadyEvent CommandLineRunner ApplicationListener onApplicationEvent() InitializingBean afterPropertiesSet() @PostConstruct ApplicationReadyEvent CommandLineRunner ApplicationListener onApplicationEvent() InitializingBean afterPropertiesSet() @PostConstruct #mermaid-svg-rPKrwQOeRbKXgMib{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rPKrwQOeRbKXgMib .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rPKrwQOeRbKXgMib .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rPKrwQOeRbKXgMib .error-icon{fill:#552222;}#mermaid-svg-rPKrwQOeRbKXgMib .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rPKrwQOeRbKXgMib .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rPKrwQOeRbKXgMib .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rPKrwQOeRbKXgMib .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rPKrwQOeRbKXgMib .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rPKrwQOeRbKXgMib .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rPKrwQOeRbKXgMib .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rPKrwQOeRbKXgMib .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rPKrwQOeRbKXgMib .marker.cross{stroke:#333333;}#mermaid-svg-rPKrwQOeRbKXgMib svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rPKrwQOeRbKXgMib p{margin:0;}#mermaid-svg-rPKrwQOeRbKXgMib .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-rPKrwQOeRbKXgMib text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-rPKrwQOeRbKXgMib .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-rPKrwQOeRbKXgMib .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-rPKrwQOeRbKXgMib .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-rPKrwQOeRbKXgMib .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-rPKrwQOeRbKXgMib #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-rPKrwQOeRbKXgMib .sequenceNumber{fill:white;}#mermaid-svg-rPKrwQOeRbKXgMib #sequencenumber{fill:#333;}#mermaid-svg-rPKrwQOeRbKXgMib #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-rPKrwQOeRbKXgMib .messageText{fill:#333;stroke:none;}#mermaid-svg-rPKrwQOeRbKXgMib .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-rPKrwQOeRbKXgMib .labelText,#mermaid-svg-rPKrwQOeRbKXgMib .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-rPKrwQOeRbKXgMib .loopText,#mermaid-svg-rPKrwQOeRbKXgMib .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-rPKrwQOeRbKXgMib .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-rPKrwQOeRbKXgMib .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-rPKrwQOeRbKXgMib .noteText,#mermaid-svg-rPKrwQOeRbKXgMib .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-rPKrwQOeRbKXgMib .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-rPKrwQOeRbKXgMib .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-rPKrwQOeRbKXgMib .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-rPKrwQOeRbKXgMib .actorPopupMenu{position:absolute;}#mermaid-svg-rPKrwQOeRbKXgMib .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-rPKrwQOeRbKXgMib .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-rPKrwQOeRbKXgMib .actor-man circle,#mermaid-svg-rPKrwQOeRbKXgMib line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-rPKrwQOeRbKXgMib :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Bean 实例化 + 注入完成之后 所有属性设置完成之后 容器刷新完成时 所有 Bean 初始化完毕 应用完全就绪 同一个 Bean 生命周期内 ContextRefreshedEvent 发布 callRunners() ApplicationReadyEvent
| 回调机制 | 执行时机 | 作用范围 | 适合做什么 | 不适合做什么 |
|---|---|---|---|---|
@PostConstruct |
当前 Bean 初始化后 | 单个 Bean | Bean 内部状态初始化 | 依赖其他 Bean 是否就绪 |
InitializingBean |
属性注入完成后 | 单个 Bean | 配置校验、复杂初始化 | 跨多个 Bean 的操作 |
ApplicationListener |
监听特定事件 | 全局 | ContextRefreshedEvent ApplicationReadyEvent | 启动初始化逻辑 |
CommandLineRunner ⭐ |
容器刷新完成后 | 全局 | 缓存预热、数据初始化、 健康检查 | 接收 HTTP 请求(此时还没 Ready) |
@EventListener(ApplicationReadyEvent) |
应用完全就绪后 | 全局 | 通知监控系统、 触发异步任务 | 必须同步完成的初始化 |
对比代码:
java
// ❌ 错误:@PostConstruct 中做数据库初始化
// @PostConstruct 执行时,DataSource 可能还没配置好
@Component
public class BadInit {
@PostConstruct
public void init() {
// 危险:其他 Bean 不一定就绪
jdbcTemplate.update("INSERT INTO ...");
}
}
// ✅ 正确:用 CommandLineRunner
// 此时所有 Bean 都已经初始化完毕
@Component
public class GoodInit implements CommandLineRunner {
@Override
public void run(String... args) {
// 安全:容器完全就绪,所有依赖都可用
jdbcTemplate.update("INSERT INTO ...");
}
}
常见踩坑与最佳实践 ⚠️
坑一:Runner 中抛异常导致应用启动失败
CommandLineRunner.run() 声明了 throws Exception,所以抛出的任何异常 都会向上传播到 SpringApplication.run(),导致应用启动失败。
java
// ❌ 危险:Redis 连不上就直接崩
@Component
public class DangerousRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// Redis 不可用时 → 异常直接导致启动失败
redisTemplate.opsForValue().set("key", "value");
}
}
// ✅ 推荐:捕获异常,记录日志,决定是否继续
@Component
public class SafeRunner implements CommandLineRunner {
@Override
public void run(String... args) {
try {
redisTemplate.opsForValue().set("key", "value");
} catch (Exception e) {
log.error("缓存预热失败,跳过(不影响启动):{}", e.getMessage());
// 不抛异常 = 应用照常启动
// 但可以发告警通知运维
}
}
}
策略选择:
| 类型 | 策略 | 示例 |
|---|---|---|
| 强依赖(数据库) | 失败 → 终止启动 | throw new IllegalStateException() |
| 弱依赖(缓存) | 失败 → 记录日志,继续启动 | catch (Exception) { log.error() } |
| 软依赖(ES 索引) | 失败 → 告警,继续启动 | catch (Exception) { alert(); } |
坑二:Runner 中执行太久,阻塞启动
callRunners() 是同步、顺序执行的,任何一个 Runner 不返回,整个启动流程就卡住了。
java
// ❌ 同步加载 100 万条数据 → 启动超时
@Component
public class SlowRunner implements CommandLineRunner {
@Override
public void run(String... args) {
List<Data> allData = dataService.loadAll(); // 耗时 5 分钟
cacheService.batchPut(allData); // 耗时 3 分钟
// K8s 健康检查超时 → Pod 被 Kill
}
}
// ✅ 大量数据用异步 + 分页
@Component
public class FastRunner implements CommandLineRunner {
@Override
public void run(String... args) {
// 只同步加载核心数据(1000 条以内)
cacheService.preloadCoreData();
// 大量数据异步加载,不阻塞启动
CompletableFuture.runAsync(() -> {
cacheService.preloadFullDataInBatches();
});
}
}
坑三:@Order 没生效
java
// ❌ @Order 失效 --- 忘记加 @Component
@Order(1)
public class SilentRunner implements CommandLineRunner { // Spring 不会扫描到它!
@Override
public void run(String... args) { ... }
}
// ✅ 正确写法
@Order(1)
@Component // ← 必须有!否则 Spring 不知道它的存在
public class LoudRunner implements CommandLineRunner { ... }
// 或者用 @Bean 注册
@Configuration
public class RunnerConfig {
@Bean
@Order(1) // Bean 方法上的 @Order 同样生效
public CommandLineRunner myRunner() {
return args -> { ... };
}
}
坑四:在 Runner 中做 RPC 调用
java
// ❌ 风险:应用还没注册到 Nacos/Eureka,Feign 调用可能失败
@Component
public class FeignInitRunner implements CommandLineRunner {
@Autowired
private UserFeignClient userClient;
@Override
public void run(String... args) {
// 此时服务可能还没注册到注册中心!
List<User> users = userClient.listAll(); // 可能超时或报错
}
}
// ✅ 用 ApplicationReadyEvent 替代
@Component
public class FeignInitListener {
@Autowired
private UserFeignClient userClient;
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
// 此时 HTTP 服务器已启动,服务已注册
List<User> users = userClient.listAll(); // 安全
}
}
最佳实践清单
#mermaid-svg-2zrlMb9lZlkTgNii{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2zrlMb9lZlkTgNii .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2zrlMb9lZlkTgNii .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2zrlMb9lZlkTgNii .error-icon{fill:#552222;}#mermaid-svg-2zrlMb9lZlkTgNii .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2zrlMb9lZlkTgNii .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2zrlMb9lZlkTgNii .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2zrlMb9lZlkTgNii .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2zrlMb9lZlkTgNii .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2zrlMb9lZlkTgNii .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2zrlMb9lZlkTgNii .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2zrlMb9lZlkTgNii .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2zrlMb9lZlkTgNii .marker.cross{stroke:#333333;}#mermaid-svg-2zrlMb9lZlkTgNii svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2zrlMb9lZlkTgNii p{margin:0;}#mermaid-svg-2zrlMb9lZlkTgNii .edge{stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .section--1 rect,#mermaid-svg-2zrlMb9lZlkTgNii .section--1 path,#mermaid-svg-2zrlMb9lZlkTgNii .section--1 circle,#mermaid-svg-2zrlMb9lZlkTgNii .section--1 polygon,#mermaid-svg-2zrlMb9lZlkTgNii .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .section--1 text{fill:#ffffff;}#mermaid-svg-2zrlMb9lZlkTgNii .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-2zrlMb9lZlkTgNii .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .edge-depth--1{stroke-width:17;}#mermaid-svg-2zrlMb9lZlkTgNii .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled,#mermaid-svg-2zrlMb9lZlkTgNii .disabled circle,#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:lightgray;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:#efefef;}#mermaid-svg-2zrlMb9lZlkTgNii .section-0 rect,#mermaid-svg-2zrlMb9lZlkTgNii .section-0 path,#mermaid-svg-2zrlMb9lZlkTgNii .section-0 circle,#mermaid-svg-2zrlMb9lZlkTgNii .section-0 polygon,#mermaid-svg-2zrlMb9lZlkTgNii .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-2zrlMb9lZlkTgNii .section-0 text{fill:black;}#mermaid-svg-2zrlMb9lZlkTgNii .node-icon-0{font-size:40px;color:black;}#mermaid-svg-2zrlMb9lZlkTgNii .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-2zrlMb9lZlkTgNii .edge-depth-0{stroke-width:14;}#mermaid-svg-2zrlMb9lZlkTgNii .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled,#mermaid-svg-2zrlMb9lZlkTgNii .disabled circle,#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:lightgray;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:#efefef;}#mermaid-svg-2zrlMb9lZlkTgNii .section-1 rect,#mermaid-svg-2zrlMb9lZlkTgNii .section-1 path,#mermaid-svg-2zrlMb9lZlkTgNii .section-1 circle,#mermaid-svg-2zrlMb9lZlkTgNii .section-1 polygon,#mermaid-svg-2zrlMb9lZlkTgNii .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .section-1 text{fill:black;}#mermaid-svg-2zrlMb9lZlkTgNii .node-icon-1{font-size:40px;color:black;}#mermaid-svg-2zrlMb9lZlkTgNii .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .edge-depth-1{stroke-width:11;}#mermaid-svg-2zrlMb9lZlkTgNii .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled,#mermaid-svg-2zrlMb9lZlkTgNii .disabled circle,#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:lightgray;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:#efefef;}#mermaid-svg-2zrlMb9lZlkTgNii .section-2 rect,#mermaid-svg-2zrlMb9lZlkTgNii .section-2 path,#mermaid-svg-2zrlMb9lZlkTgNii .section-2 circle,#mermaid-svg-2zrlMb9lZlkTgNii .section-2 polygon,#mermaid-svg-2zrlMb9lZlkTgNii .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .section-2 text{fill:#ffffff;}#mermaid-svg-2zrlMb9lZlkTgNii .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-2zrlMb9lZlkTgNii .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .edge-depth-2{stroke-width:8;}#mermaid-svg-2zrlMb9lZlkTgNii .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled,#mermaid-svg-2zrlMb9lZlkTgNii .disabled circle,#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:lightgray;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:#efefef;}#mermaid-svg-2zrlMb9lZlkTgNii .section-3 rect,#mermaid-svg-2zrlMb9lZlkTgNii .section-3 path,#mermaid-svg-2zrlMb9lZlkTgNii .section-3 circle,#mermaid-svg-2zrlMb9lZlkTgNii .section-3 polygon,#mermaid-svg-2zrlMb9lZlkTgNii .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .section-3 text{fill:black;}#mermaid-svg-2zrlMb9lZlkTgNii .node-icon-3{font-size:40px;color:black;}#mermaid-svg-2zrlMb9lZlkTgNii .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .edge-depth-3{stroke-width:5;}#mermaid-svg-2zrlMb9lZlkTgNii .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled,#mermaid-svg-2zrlMb9lZlkTgNii .disabled circle,#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:lightgray;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:#efefef;}#mermaid-svg-2zrlMb9lZlkTgNii .section-4 rect,#mermaid-svg-2zrlMb9lZlkTgNii .section-4 path,#mermaid-svg-2zrlMb9lZlkTgNii .section-4 circle,#mermaid-svg-2zrlMb9lZlkTgNii .section-4 polygon,#mermaid-svg-2zrlMb9lZlkTgNii .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .section-4 text{fill:black;}#mermaid-svg-2zrlMb9lZlkTgNii .node-icon-4{font-size:40px;color:black;}#mermaid-svg-2zrlMb9lZlkTgNii .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .edge-depth-4{stroke-width:2;}#mermaid-svg-2zrlMb9lZlkTgNii .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled,#mermaid-svg-2zrlMb9lZlkTgNii .disabled circle,#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:lightgray;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:#efefef;}#mermaid-svg-2zrlMb9lZlkTgNii .section-5 rect,#mermaid-svg-2zrlMb9lZlkTgNii .section-5 path,#mermaid-svg-2zrlMb9lZlkTgNii .section-5 circle,#mermaid-svg-2zrlMb9lZlkTgNii .section-5 polygon,#mermaid-svg-2zrlMb9lZlkTgNii .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .section-5 text{fill:black;}#mermaid-svg-2zrlMb9lZlkTgNii .node-icon-5{font-size:40px;color:black;}#mermaid-svg-2zrlMb9lZlkTgNii .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .edge-depth-5{stroke-width:-1;}#mermaid-svg-2zrlMb9lZlkTgNii .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled,#mermaid-svg-2zrlMb9lZlkTgNii .disabled circle,#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:lightgray;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:#efefef;}#mermaid-svg-2zrlMb9lZlkTgNii .section-6 rect,#mermaid-svg-2zrlMb9lZlkTgNii .section-6 path,#mermaid-svg-2zrlMb9lZlkTgNii .section-6 circle,#mermaid-svg-2zrlMb9lZlkTgNii .section-6 polygon,#mermaid-svg-2zrlMb9lZlkTgNii .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .section-6 text{fill:black;}#mermaid-svg-2zrlMb9lZlkTgNii .node-icon-6{font-size:40px;color:black;}#mermaid-svg-2zrlMb9lZlkTgNii .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .edge-depth-6{stroke-width:-4;}#mermaid-svg-2zrlMb9lZlkTgNii .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled,#mermaid-svg-2zrlMb9lZlkTgNii .disabled circle,#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:lightgray;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:#efefef;}#mermaid-svg-2zrlMb9lZlkTgNii .section-7 rect,#mermaid-svg-2zrlMb9lZlkTgNii .section-7 path,#mermaid-svg-2zrlMb9lZlkTgNii .section-7 circle,#mermaid-svg-2zrlMb9lZlkTgNii .section-7 polygon,#mermaid-svg-2zrlMb9lZlkTgNii .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .section-7 text{fill:black;}#mermaid-svg-2zrlMb9lZlkTgNii .node-icon-7{font-size:40px;color:black;}#mermaid-svg-2zrlMb9lZlkTgNii .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .edge-depth-7{stroke-width:-7;}#mermaid-svg-2zrlMb9lZlkTgNii .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled,#mermaid-svg-2zrlMb9lZlkTgNii .disabled circle,#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:lightgray;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:#efefef;}#mermaid-svg-2zrlMb9lZlkTgNii .section-8 rect,#mermaid-svg-2zrlMb9lZlkTgNii .section-8 path,#mermaid-svg-2zrlMb9lZlkTgNii .section-8 circle,#mermaid-svg-2zrlMb9lZlkTgNii .section-8 polygon,#mermaid-svg-2zrlMb9lZlkTgNii .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .section-8 text{fill:black;}#mermaid-svg-2zrlMb9lZlkTgNii .node-icon-8{font-size:40px;color:black;}#mermaid-svg-2zrlMb9lZlkTgNii .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .edge-depth-8{stroke-width:-10;}#mermaid-svg-2zrlMb9lZlkTgNii .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled,#mermaid-svg-2zrlMb9lZlkTgNii .disabled circle,#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:lightgray;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:#efefef;}#mermaid-svg-2zrlMb9lZlkTgNii .section-9 rect,#mermaid-svg-2zrlMb9lZlkTgNii .section-9 path,#mermaid-svg-2zrlMb9lZlkTgNii .section-9 circle,#mermaid-svg-2zrlMb9lZlkTgNii .section-9 polygon,#mermaid-svg-2zrlMb9lZlkTgNii .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .section-9 text{fill:black;}#mermaid-svg-2zrlMb9lZlkTgNii .node-icon-9{font-size:40px;color:black;}#mermaid-svg-2zrlMb9lZlkTgNii .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .edge-depth-9{stroke-width:-13;}#mermaid-svg-2zrlMb9lZlkTgNii .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled,#mermaid-svg-2zrlMb9lZlkTgNii .disabled circle,#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:lightgray;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:#efefef;}#mermaid-svg-2zrlMb9lZlkTgNii .section-10 rect,#mermaid-svg-2zrlMb9lZlkTgNii .section-10 path,#mermaid-svg-2zrlMb9lZlkTgNii .section-10 circle,#mermaid-svg-2zrlMb9lZlkTgNii .section-10 polygon,#mermaid-svg-2zrlMb9lZlkTgNii .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .section-10 text{fill:black;}#mermaid-svg-2zrlMb9lZlkTgNii .node-icon-10{font-size:40px;color:black;}#mermaid-svg-2zrlMb9lZlkTgNii .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .edge-depth-10{stroke-width:-16;}#mermaid-svg-2zrlMb9lZlkTgNii .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled,#mermaid-svg-2zrlMb9lZlkTgNii .disabled circle,#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:lightgray;}#mermaid-svg-2zrlMb9lZlkTgNii .disabled text{fill:#efefef;}#mermaid-svg-2zrlMb9lZlkTgNii .section-root rect,#mermaid-svg-2zrlMb9lZlkTgNii .section-root path,#mermaid-svg-2zrlMb9lZlkTgNii .section-root circle,#mermaid-svg-2zrlMb9lZlkTgNii .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-2zrlMb9lZlkTgNii .section-root text{fill:#ffffff;}#mermaid-svg-2zrlMb9lZlkTgNii .section-root span{color:#ffffff;}#mermaid-svg-2zrlMb9lZlkTgNii .section-2 span{color:#ffffff;}#mermaid-svg-2zrlMb9lZlkTgNii .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-2zrlMb9lZlkTgNii .edge{fill:none;}#mermaid-svg-2zrlMb9lZlkTgNii .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-2zrlMb9lZlkTgNii :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CommandLineRunner
最佳实践
异常处理
强依赖失败 → 终止启动
弱依赖失败 → 记录 + 继续
执行策略
核心数据 → 同步加载(< 1000 条)
大量数据 → 异步加载
0
安全
敏感操作记录审计日志
避免在 Runner 中暴露异常堆栈
可观测
打印每个 Runner 的开始/结束/耗时
Runner 失败时发告警
单元测试
用 @SpringBootTest 测试 Runner
或 Mock 依赖进行单元测试
总结速查卡
#mermaid-svg-35Hn4F5svCKGltUu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-35Hn4F5svCKGltUu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-35Hn4F5svCKGltUu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-35Hn4F5svCKGltUu .error-icon{fill:#552222;}#mermaid-svg-35Hn4F5svCKGltUu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-35Hn4F5svCKGltUu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-35Hn4F5svCKGltUu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-35Hn4F5svCKGltUu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-35Hn4F5svCKGltUu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-35Hn4F5svCKGltUu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-35Hn4F5svCKGltUu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-35Hn4F5svCKGltUu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-35Hn4F5svCKGltUu .marker.cross{stroke:#333333;}#mermaid-svg-35Hn4F5svCKGltUu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-35Hn4F5svCKGltUu p{margin:0;}#mermaid-svg-35Hn4F5svCKGltUu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-35Hn4F5svCKGltUu .cluster-label text{fill:#333;}#mermaid-svg-35Hn4F5svCKGltUu .cluster-label span{color:#333;}#mermaid-svg-35Hn4F5svCKGltUu .cluster-label span p{background-color:transparent;}#mermaid-svg-35Hn4F5svCKGltUu .label text,#mermaid-svg-35Hn4F5svCKGltUu span{fill:#333;color:#333;}#mermaid-svg-35Hn4F5svCKGltUu .node rect,#mermaid-svg-35Hn4F5svCKGltUu .node circle,#mermaid-svg-35Hn4F5svCKGltUu .node ellipse,#mermaid-svg-35Hn4F5svCKGltUu .node polygon,#mermaid-svg-35Hn4F5svCKGltUu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-35Hn4F5svCKGltUu .rough-node .label text,#mermaid-svg-35Hn4F5svCKGltUu .node .label text,#mermaid-svg-35Hn4F5svCKGltUu .image-shape .label,#mermaid-svg-35Hn4F5svCKGltUu .icon-shape .label{text-anchor:middle;}#mermaid-svg-35Hn4F5svCKGltUu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-35Hn4F5svCKGltUu .rough-node .label,#mermaid-svg-35Hn4F5svCKGltUu .node .label,#mermaid-svg-35Hn4F5svCKGltUu .image-shape .label,#mermaid-svg-35Hn4F5svCKGltUu .icon-shape .label{text-align:center;}#mermaid-svg-35Hn4F5svCKGltUu .node.clickable{cursor:pointer;}#mermaid-svg-35Hn4F5svCKGltUu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-35Hn4F5svCKGltUu .arrowheadPath{fill:#333333;}#mermaid-svg-35Hn4F5svCKGltUu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-35Hn4F5svCKGltUu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-35Hn4F5svCKGltUu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-35Hn4F5svCKGltUu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-35Hn4F5svCKGltUu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-35Hn4F5svCKGltUu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-35Hn4F5svCKGltUu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-35Hn4F5svCKGltUu .cluster text{fill:#333;}#mermaid-svg-35Hn4F5svCKGltUu .cluster span{color:#333;}#mermaid-svg-35Hn4F5svCKGltUu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-35Hn4F5svCKGltUu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-35Hn4F5svCKGltUu rect.text{fill:none;stroke-width:0;}#mermaid-svg-35Hn4F5svCKGltUu .icon-shape,#mermaid-svg-35Hn4F5svCKGltUu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-35Hn4F5svCKGltUu .icon-shape p,#mermaid-svg-35Hn4F5svCKGltUu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-35Hn4F5svCKGltUu .icon-shape .label rect,#mermaid-svg-35Hn4F5svCKGltUu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-35Hn4F5svCKGltUu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-35Hn4F5svCKGltUu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-35Hn4F5svCKGltUu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 注意事项
异常会导致启动失败
同步执行,不要阻塞太久
RPC 调用用 ApplicationReadyEvent
顺序控制
@Order(1)、@Order(2)...
数字越小越先执行
什么时候用
缓存预热
数据初始化
健康检查
索引/表结构检查
怎么用
实现接口
implements CommandLineRunner
Lambda
@Bean 返回 Lambda
是什么
CommandLineRunner
Spring Boot 启动回调接口
容器初始化后、接流量前执行
| 维度 | 要点 |
|---|---|
| 一句话 | 容器就绪后、接流量前,做最后一次初始化 |
| 接口方法 | void run(String... args) throws Exception |
| 参数 | main 方法的原始命令行参数 |
| vs ApplicationRunner | 参数是 String[] 而非 ApplicationArguments |
| 执行顺序 | @Order(1) → @Order(2) → ... 数字小的先执行 |
| vs @PostConstruct | Runner 在所有 Bean 初始化完毕才执行,更安全 |
| 典型场景 | 预热缓存、初始化数据、启动健康检查、ES 索引创建 |
| 核心坑 | Runner 抛异常 → 应用启动失败;长时间阻塞 → K8s 超时 Kill |