前言
Spring 应用有时会在应用启动后做一些初始化的操作,比如从数据库中拉取一些数据缓存起来,比如读取一些配置变量。如何在容器启动后来执行一个任务呢?本文针对这个问题,探讨一下几个方面的内容。
- Spring 是如何监听启动事件的?
- Spring Boot 中的 ApplicationRunner 和 CommandLineRunner 是什么?
- ApplicationRunner 和 CommandLineRunner 的区别。
一、监听 ContextRefreshedEvent
如果要在容器启动后做一些操作,第一直觉就是使用监听器监听容器的启动事件,在回调函数中完成任务。Spring 中我们也是这么做的。通过监听 ContextRefreshedEvent(该事件发生在容器初始化完毕后)实现自定义的初始化逻辑。
以上代码能生效的原因是,Spring 在初始化 ApplicationContext 的时候,会从当前的 bean 中找到 ApplicationListener 类型的 bean,将这些 bean 注册到 ApplicationContext 的事件发布器上。
ContextRefreshedEvent 事件是 ApplicationContextEvent 的一个子类,ApplicationContextEvent 的子类有很多,分别表示了 ApplicationContext 生命周期的不同阶段。ContextRefreshedEvent 事件发生在容器初始化完毕后。此时 Spring 已经将所有的 bean 被成功加载,我们可以在这个监听器中注入我们要用到的 bean,就像写正常的业务代码一样,完成启动后的初始化任务。
监听 ContextRefreshedEvent 事件的方式在 Spring 和 Spring Boot 中都行的通。不仅如此,我们还可以监听各种的 ApplicationContextEvent,比如监听 ContextStoppedEvent,用于容器销毁是删除一些副作用。
二、ApplicationRunner 和 CommandLineRunner 的用法
除了监听事件外,Spring Boot 其实还提供了两个接口,专门用于完成启动后的初始化工作,那就是 ApplicationRunner 和 CommandLineRunner。这两个接口的用法是一样的,继承后实现 run 方法,这个 run 方法会在容器初始化完毕后执行。它们还实现了 Order 接口,可以自定义执行顺序。
Spring Boot 这么设计,其实是为了概念上将 Context 事件和应用初始化做分隔,因为在 ContextRefreshedEvent 事件发生的时候,只是 bean 的上下文环境配置好了,并这并不是容器启动的最后一步,后续还有一些行为,比如 SpringApplicationRunListener 会发出事件等。我们监听 ContextRefreshedEvent 事件,能实现执行初始化任务的目标,但在语义上两者是不一致的。
ApplicationRunner 和 CommandLineRunner 是 Spring Boot 提供的专门用于处理启动后的初始化工作的接口,他们的执行一定是在容器启动的最后一步。也就是 run 方法的最后一步。
callRunners 中就是对 ApplicationRunner 和 CommandLineRunner 类的调用,Spring 从当前的 bean 集合中拿出类型为 ApplicationRunner 和 CommandLineRunner 的实例,将其放到一个列表中,然后根据 order 申明排序,依次执行 bean 的 run 方法。
从代码中可以看出,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 就足够了。
总结
- 1、Spring 基于监听 ContextRefreshedEvent 事件,在应用启动后完成初始化操作。Spring Boot 中也能使用这种方式。
- 2、Spring Boot 提供了 ApplicationRunner 和 CommandLineRunner 用于完成启动后的初始化工作,我们只要实现继承这个接口并实现其中的 run 方法就可以了。
- 3、ApplicationRunner 和 CommandLineRunner 都可以获得 Spring Boot 入口的传参,两者的区别是,前者通过 ApplicationArguments 对参数进行了简单处理,而后者获得参数经过切分的数组。