为什么我的CommandLineRunner 不 run ?

本文深入探讨了Spring Boot中CommandLineRunner的工作原理,并通过具体案例揭示了为何某些实现类可能会阻止其他Runner的执行。文章还提供了解决方案,确保每个Runner都能按预期运行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作者:jwenjian
链接:https://juejin.im/post/5e6205b651882549215ce3a9
来源:掘金

 

Springboot 工程中通常都有几个 CommandLineRunner 的实现类,用来在程序启动之后干点什么。但如果使用不当,可能就会发现有的 Runner 在程序启动之后执行了,有的却没有执行,更奇怪的是程序也没有报错。原因就是...

先看现象

假设一个工程中需要用到 3 个 CommandLineRunner, 其中 2 个 runner 需要执行一个 while(true) 循环

代码如下:

 

@Component
public class Runner1 implements CommandLineRunner {
    public void run(String... args) throws Exception {
        log.info("runner 1 running...");
    }
}

@Component
public class Runner2 implements CommandLineRunner {
    public void run(String... args) throws Exception {
        log.info("runner 2 running...")
        while(true) {
            doSomething();
        }
    }
}

@Component
public class Runner3 implements CommandLineRunner {
    public void run(String... args) throws Exception {
        log.info("runner 3 running...")
        while(true) {
            doOtherThing();
        }
    }
}

程序运行之后 你会发现 Runner2 和 Runnner3 总会有一个不运行,甚至 Runner1 也不运行,但是程序也不报错,怎么回事?

透过本质看现象

是的,先看 CommandLineRunner 是怎么执行的才能知道错在了哪里从而解决这个问题。

CommandLineRunner.java

 

/**
 * Interface used to indicate that a bean should <em>run</em> when it is contained within
 * a {@link SpringApplication}. Multiple {@link CommandLineRunner} beans can be defined
 * within the same application context and can be ordered using the {@link Ordered}
 * interface or {@link Order @Order} annotation.
 * <p>
 * If you need access to {@link ApplicationArguments} instead of the raw String array
 * consider using {@link ApplicationRunner}.
 *
 * @author Dave Syer
 * @since 1.0.0
 * @see ApplicationRunner
 */
@FunctionalInterface
public interface CommandLineRunner {

    /**
     * Callback used to run the bean.
     * @param args incoming main method arguments
     * @throws Exception on error
     */
    void run(String... args) throws Exception;

}

注释第一句话表示:实现了这个接口的一个 spring bean 在程序启动之后应该 run.

这里的 run 用了 <em> 标签进行了强调,为什么?不着急回答,再往下看 看看 CommandLineRunner 是如何被Spring Boot 执行的...

SpringApplication.java

接下来看看 SpringApplication 这个类的注释信息:

 

* Class that can be used to bootstrap and launch a Spring application from a Java main
 * method. By default class will perform the following steps to bootstrap your
 * application:
 *
 * <ul>
 * <li>Create an appropriate {@link ApplicationContext} instance (depending on your
 * classpath)</li>
 * <li>Register a {@link CommandLinePropertySource} to expose command line arguments as
 * Spring properties</li>
 * <li>Refresh the application context, loading all singleton beans</li>
 * <li>Trigger any {@link CommandLineRunner} beans</li>
 * </ul>

从这个注释可以知道,SpringApplication 负责触发所有 CommandLineRunner 的运行。到代码部分,就是下面的几个方法:

  • callRunners()

 

private void callRunners(ApplicationContext context, ApplicationArguments args) {
        List<Object> runners = new ArrayList<>();
        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
        runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
        AnnotationAwareOrderComparator.sort(runners);
        for (Object runner : new LinkedHashSet<>(runners)) {
            if (runner instanceof ApplicationRunner) {
                callRunner((ApplicationRunner) runner, args);
            }
            if (runner instanceof CommandLineRunner) {
                callRunner((CommandLineRunner) runner, args);
            }
        }
    }
  • callRunner()

 

private void callRunner(ApplicationRunner runner, ApplicationArguments args) {
        try {
            (runner).run(args);
        }
        catch (Exception ex) {
            throw new IllegalStateException("Failed to execute ApplicationRunner", ex);
        }
    }

    private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
        try {
            (runner).run(args.getSourceArgs());
        }
        catch (Exception ex) {
            throw new IllegalStateException("Failed to execute CommandLineRunner", ex);
        }
    }

callRunners 负责从上下文中获取到所有 CommandLineRunner 的实现类,循环遍历,对于每一个实现类对象,调用 callRunner 方法进行触发。而在 callRunner 方法中同步执行的是 runner 对象的 run 方法.

到此,答案已经很清晰了,CommandLineRunner 不 run ,或者有的 run 有的不 run,程序也不报错的原因就是:

  1. 你有多个 CommandLineRunner 的实现类
  2. 在某个实现类的 run 方法体中调用了同步阻塞的API或者是一个 while(true) 循环

这样就导致了 SpringApplication 在执行这个 run 方法时阻塞,无法继续执行循环中的下一个 runner 的 run 方法。

为了避免程序中犯这个错,一定要记住,

CommandLineRunner != Runnable

CommandLineRunner != Runnable

CommandLineRunner != Runnable

虽然这两个接口都有一个 run 方法,很容易让人理解成一个 CommandLineRunner 就会对应一个线程。NO!

知错就改

对于 Runner1 不需要做任何修改。 对于 Runner2 和 Runner3,需要做以下修改,让 while(true) 循环不阻塞 run 方法的运行即可。

 

@Component
public class Runner2 implements CommandLineRunner {
    public void run(String... args) throws Exception {
        log.info("runner 2 running...")
        new Thread(() -> {
            while(true) {
                doSomething();
            }
        }).start();
    }
}

@Component
public class Runner3 implements CommandLineRunner {
    public void run(String... args) throws Exception {
        log.info("runner 3 running...")
        new Thread(() -> {
            while(true) {
                doOtherThing();
            }
        }).start();
    }
}


 

### 解决 Spring Boot 中 `CommandLineRunner` 方法未执行的问题 当遇到 `CommandLineRunner` 接口方法未能按预期运行的情况时,可能涉及多个方面的原因。以下是详细的排查和解决方案: #### 1. 确认实现类被扫描到 为了确保实现了 `CommandLineRunner` 的类能够正常工作,必须确认此类位于组件扫描路径下。如果应用的包结构如下所示,则无需额外配置。 假设项目根目录下的包名为 `com.example.demo`,而 `CommandLineRunner` 实现类也在此包内或其子包中[^1]。 ```java package com.example.demo; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class MyRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("This is a test message from the command line runner."); } } ``` #### 2. 验证主应用程序位置 主应用程序类的位置非常重要。通常情况下,建议将主应用程序放置于最顶层的包中,以便自动发现其他组件和服务。例如,在上述例子中的 `OrderMain80` 和 `PaymentMain9003` 类都应处于顶级包之下[^2]。 #### 3. 使用 `@ComponentScan` 注解(如有必要) 如果自定义了包名或者有特殊需求,可以考虑显式指定要扫描哪些包来查找带有特定注解的bean。这可以通过在主程序上添加 `@ComponentScan(basePackages="your.package.name")` 来完成。 #### 4. 检查日志输出和其他异常情况 查看控制台的日志信息可以帮助定位问题所在。如果有任何错误堆栈跟踪或者其他警告提示,这些可能是导致 `CommandLineRunner` 被执行的根本原因。此外,还可以通过设置更详细的日志级别来获取更多信息。 #### 5. 调试模式启用 开启调试模式有助于更好地理解整个启动过程以及各个阶段的状态变化。可以在命令行参数里加入 `-Ddebug=true` 或者修改 application.properties 文件以激活它。 ```properties # application.properties debug=true ``` #### 6. 多个 `CommandLineRunner` 的顺序管理 若有多个 `CommandLineRunner` 存在于同一个上下文中,默认会按照它们注册进容器里的先后次序依次调用。对于这种情况,可利用 `org.springframework.core.annotation.Order` 注解来自定义优先级。 ```java package com.example.demo; import org.springframework.boot.CommandLineRunner; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Component @Order(1) public class FirstRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("First Runner executed"); } } @Component @Order(2) public class SecondRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("Second Runner executed"); } } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值