【Spring】CommandLineRunner接口

本文深入讲解 Spring Boot 的 CommandLineRunner 接口:从源码、基础使用、执行顺序到缓存预热、数据库初始化等实战场景,以及常见踩坑与最佳实践,帮你彻底掌握启动回调机制。

CommandLineRunner接口

  • Hook
  • 接口定义(源码速览)
  • 基础使用
    • [方式一:实现 CommandLineRunner 接口](#方式一:实现 CommandLineRunner 接口)
    • [方式二:@Bean + Lambda(推荐)](#方式二:@Bean + Lambda(推荐))
  • [CommandLineRunner vs ApplicationRunner ⭐](#CommandLineRunner vs ApplicationRunner ⭐)
  • [多个 Runner 的执行顺序](#多个 Runner 的执行顺序)
  • 实战场景
  • [源码解析: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("===== 缓存预热完成 =====");
        };
    }
}

什么时候用 @Bean Lambda 写法? 逻辑简单、不跨多个 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 的顺序,不可靠)
混用 CommandLineRunnerApplicationRunner 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);
        }
    }
}

关键发现ApplicationRunnerCommandLineRunner 是混在一起排序的,不区分类型 。所以如果你有 @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

参考链接