原文:https://zhuanlan.zhihu.com/p/352967633
作者:神马翔
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("容器初始化完毕"); } }
以上代码能生效的原因是,Spring 在初始化 ApplicationContext 的时候,会从当前的 bean 中找到 ApplicationListener 类型的 bean,将这些 bean 注册到 ApplicationContext 的事件发布器上。
protected void registerListeners() { ... String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false); for (String listenerBeanName : listenerBeanNames) { getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName); } .. }
ContextRefreshedEvent 事件是 ApplicationContextEvent 的一个子类,ApplicationContextEvent 的子类有很多,分别表示了 ApplicationContext 生命周期的不同阶段。ContextRefreshedEvent 事件发生在容器初始化完毕后。此时 Spring 已经将所有的 bean 被成功加载,我们可以在这个监听器中注入我们要用到的 bean,就像写正常的业务代码一样,完成启动后的初始化任务。
监听 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("初始化代码"); } }
Spring Boot 这么设计,其实是为了概念上将 Context 事件和应用初始化做分隔,因为在 ContextRefreshedEvent 事件发生的时候,只是 bean 的上下文环境配置好了,并这并不是容器启动的最后一步,后续还有一些行为,比如 SpringApplicationRunListener 会发出事件等。我们监听 ContextRefreshedEvent 事件,能实现执行初始化任务的目标,但在语义上两者是不一致的。
ApplicationRunner 和 CommandLineRunner 是 Spring Boot 提供的专门用于处理启动后的初始化工作的接口,他们的执行一定是在容器启动的最后一步。也就是 run 方法的最后一步。
public ConfigurableApplicationContext run(String... args) { ... try { ... callRunners(context, applicationArguments); } ... }
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); } } }
从代码中可以看出,ApplicationRunner 和 CommandLineRunner 的执行顺序是按照 Order 接口设定的值来的,如果 Order 相同,那么 ApplicationRunner 先执行,因为是 ApplicationRunner 先被加入到 runners 列表中。
ApplicationRunner 和 CommandLineRunner 的区别
既然都是执行初始化任务,那么为什么不合并为一个接口?这两个接口的不同之处在于:ApplicationRunner 中 run 方法的参数为 ApplicationArguments,而 CommandLineRunner 接口中 run 方法的参数为 String 数组。
这里的参数指的就是 Spring Boot 主函数的参数,我们可以在 IDEA 的 【Run/Debug Configurations】中设置这个参数。配置方式是 --key=value 的形式。多个参数用空格隔开。
如果使用了 CommandLineRunner,那么 run 方法的入参就是我们这里配置的参数。
参数会被 Spring Boot 转换为 ApplicationArguments 对象,这个对象会被加入 bean 集合中,所以我们可以通过 spring 注入 ApplicationArguments 来获得 main 方法的入参。这个对象同时也是 ApplicationRunner 的入参。
我们之所以将配置写成 --key=value 的形式,原因在于 ApplicationArguments 对象中就是这么解析的,写成其它格式的,那么该对象就不会帮我们解析了。
综上,这两个 runner 的区别其实不大,只不过 ApplicationArguments 中获得的参数经过了简单的转换,而 CommandLineRunner 需要自己处理这些参数。通过命名也可以看出,CommandLineRunner 着重命令行,可能是简单的 key value 的处理方式不满足需求,是个复杂的命令,需要自定义处理方案。一般使用 ApplicationRunner 就足够了。
总结
- Spring 基于监听 ContextRefreshedEvent 事件,在应用启动后完成初始化操作。Spring Boot 中也能使用这种方式。
- Spring Boot 提供了 ApplicationRunner 和 CommandLineRunner 用于完成启动后的初始化工作,我们只要实现继承这个接口并实现其中的 run 方法就可以了。
- ApplicationRunner 和 CommandLineRunner 都可以获得 Spring Boot 入口的传参,两者的区别是,前者通过 ApplicationArguments 对参数进行了简单处理,而后者获得参数经过切分的数组。