前言

Spring 应用有时会在应用启动后做一些初始化的操作,比如从数据库中拉取一些数据缓存起来,比如读取一些配置变量。如何在容器启动后来执行一个任务呢?本文针对这个问题,探讨一下几个方面的内容。

  • Spring 是如何监听启动事件的?
  • Spring Boot 中的 ApplicationRunner 和 CommandLineRunner 是什么?
  • ApplicationRunner 和 CommandLineRunner 的区别。

一、监听 ContextRefreshedEvent

如果要在容器启动后做一些操作,第一直觉就是使用监听器监听容器的启动事件,在回调函数中完成任务。Spring 中我们也是这么做的。通过监听 ContextRefreshedEvent(该事件发生在容器初始化完毕后)实现自定义的初始化逻辑。

@Component
public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("容器初始化完毕");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

以上代码能生效的原因是,Spring 在初始化 ApplicationContext 的时候,会从当前的 bean 中找到 ApplicationListener 类型的 bean,将这些 bean 注册到 ApplicationContext 的事件发布器上。

protected void registerListeners() {
  ...
  String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
  for (String listenerBeanName : listenerBeanNames) {
    getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
  }
  ..
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

ContextRefreshedEvent 事件是 ApplicationContextEvent 的一个子类,ApplicationContextEvent 的子类有很多,分别表示了 ApplicationContext 生命周期的不同阶段。ContextRefreshedEvent 事件发生在容器初始化完毕后。此时 Spring 已经将所有的 bean 被成功加载,我们可以在这个监听器中注入我们要用到的 bean,就像写正常的业务代码一样,完成启动后的初始化任务。SpringBoot——Spring Boot 如何在启动后执行初始化任务_自定义

监听 ContextRefreshedEvent 事件的方式在 Spring 和 Spring Boot 中都行的通。不仅如此,我们还可以监听各种的 ApplicationContextEvent,比如监听 ContextStoppedEvent,用于容器销毁是删除一些副作用。

二、ApplicationRunner 和 CommandLineRunner 的用法

除了监听事件外,Spring Boot 其实还提供了两个接口,专门用于完成启动后的初始化工作,那就是 ApplicationRunner 和 CommandLineRunner。这两个接口的用法是一样的,继承后实现 run 方法,这个 run 方法会在容器初始化完毕后执行。它们还实现了 Order 接口,可以自定义执行顺序。

@Order(1)
@Component
public class AppStartupRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("初始化代码");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

Spring Boot 这么设计,其实是为了概念上将 Context 事件和应用初始化做分隔,因为在 ContextRefreshedEvent 事件发生的时候,只是 bean 的上下文环境配置好了,并这并不是容器启动的最后一步,后续还有一些行为,比如 SpringApplicationRunListener 会发出事件等。我们监听 ContextRefreshedEvent 事件,能实现执行初始化任务的目标,但在语义上两者是不一致的。

ApplicationRunner 和 CommandLineRunner 是 Spring Boot 提供的专门用于处理启动后的初始化工作的接口,他们的执行一定是在容器启动的最后一步。也就是 run 方法的最后一步。

public ConfigurableApplicationContext run(String... args) {
        ...
        try {
            ...
            callRunners(context, applicationArguments);
        }
        ...
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

callRunners 中就是对 ApplicationRunner 和 CommandLineRunner 类的调用,Spring 从当前的 bean 集合中拿出类型为 ApplicationRunner 和 CommandLineRunner 的实例,将其放到一个列表中,然后根据 order 申明排序,依次执行 bean 的 run 方法。

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);
  // 执行 run 方法
  for (Object runner : new LinkedHashSet<>(runners)) {
    if (runner instanceof ApplicationRunner) {
      callRunner((ApplicationRunner) runner, args);
    }
    if (runner instanceof CommandLineRunner) {
      callRunner((CommandLineRunner) runner, args);
    }
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

从代码中可以看出,ApplicationRunner 和 CommandLineRunner 的执行顺序是按照 Order 接口设定的值来的,如果 Order 相同,那么 ApplicationRunner 先执行,因为是 ApplicationRunner 先被加入到 runners 列表中。

三、ApplicationRunner 和 CommandLineRunner 的区别

既然都是执行初始化任务,那么为什么不合并为一个接口?这两个接口的不同之处在于:ApplicationRunner 中 run 方法的参数为 ApplicationArguments,而 CommandLineRunner 接口中 run 方法的参数为 String 数组。

这里的参数指的就是 Spring Boot 主函数的参数,我们可以在 IDEA 的 【Run/Debug Configurations】中设置这个参数。配置方式是 --key=value 的形式。多个参数用空格隔开。SpringBoot——Spring Boot 如何在启动后执行初始化任务_自定义_02

如果使用了 CommandLineRunner,那么 run 方法的入参就是我们这里配置的参数。

参数会被 Spring Boot 转换为 ApplicationArguments 对象,这个对象会被加入 bean 集合中,所以我们可以通过 spring 注入 ApplicationArguments 来获得 main 方法的入参。这个对象同时也是 ApplicationRunner 的入参。

我们之所以将配置写成 --key=value 的形式,原因在于 ApplicationArguments 对象中就是这么解析的,写成其它格式的,那么该对象就不会帮我们解析了。

综上,这两个 runner 的区别其实不大,只不过 ApplicationArguments 中获得的参数经过了简单的转换,而 CommandLineRunner 需要自己处理这些参数。通过命名也可以看出,CommandLineRunner 着重命令行,可能是简单的 key value 的处理方式不满足需求,是个复杂的命令,需要自定义处理方案。一般使用 ApplicationRunner 就足够了。

总结

  • 1、Spring 基于监听 ContextRefreshedEvent 事件,在应用启动后完成初始化操作。Spring Boot 中也能使用这种方式。
  • 2、Spring Boot 提供了 ApplicationRunner 和 CommandLineRunner 用于完成启动后的初始化工作,我们只要实现继承这个接口并实现其中的 run 方法就可以了。
  • 3、ApplicationRunner 和 CommandLineRunner 都可以获得 Spring Boot 入口的传参,两者的区别是,前者通过 ApplicationArguments 对参数进行了简单处理,而后者获得参数经过切分的数组。

参考: https://zhuanlan.zhihu.com/p/352967633