摘要
在实际项目中,我们需要在 springboot 服务启动后做一些初始化工作,例如线程池初始化、文件资源加载、常驻后台任务启动(比如 kafka consumer)等。本文介绍 3 类初始化资源的方法:
- Spring Bean 初始化的 InitializingBean,init-method 和 PostConstruct
-
ApplicationRunner
与CommandLineRunner
接口 - Spring 的事件机制
方法 1:spring bean 初始化
Spring 容器中的 Bean 是有生命周期的,Spring 允许在 Bean 在初始化完成后以及 Bean 销毁前执行特定的操作,常用的设定方式有以下三种:
- 通过实现 InitializingBean 接口来定制初始化之后的操作方法;
- 通过 <bean> 元素的 init-method 属性指定初始化之后调用的操作方法;
- 在指定方法上加上 @PostConstruct 注解来指定该在初始化之后调用的方法
执行的先后顺序:构造方法 –> @PostConstruct
注解的方法 –> afterPropertiesSet
方法(InitializingBean
接口)–> init-method
指定的方法。详情参考 Spring Bean 初始化之 InitializingBean, init-method 和 PostConstruct
方法 2:ApplicationRunner
与 CommandLineRunner
接口
CommandLineRunner
/ApplicationRunner
接口的 Component
会在所有 Spring Beans
都初始化之后,SpringApplication.run()
之前执行,非常适合在应用程序启动之初进行一些数据初始化的工作。CommandLineRunner
和 ApplicationRunner
这两个接口工作方式相同,都只提供单一的 run 方法,唯一的区别是 run 方法的入参类型不同,CommandLineRunner
的参数是最原始的参数,没有进行任何处理,ApplicationRunner
的参数是ApplicationArguments
, 是对原始参数的进一步封装。
runner 定义
以下定义了两个 runner,其中 Order
注解指定了执行顺序,数字越小越先执行
@Order(1)
@Component
@Slf4j
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) {log.info("MyCommandLineRunner run...");
}
}
-------------------------------------------------------------------
@Order(2)
@Component
@Slf4j
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {log.info("MyApplicationRunner run...");
}
}
源码跟踪
跟踪源码,看下 CommandLineRunner
/ApplicationRunner
是如何被调用的,Springboot 在启动的时候,都会构造一个 SpringApplication
实例,执行 run 方法,一路点进去后不难发现调用入口是 SpringApplication.run
方法中的callRunners(context, applicationArguments)
public ConfigurableApplicationContext run(String... args) {
......
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {handleRunFailure(context, listeners, exceptionReporters, ex);
throw new IllegalStateException(ex);
}
listeners.running(context);
return context;
}
总结
- 所有 CommandLineRunner/ApplicationRunner 的执行时间点是在 SpringBoot 应用的 ApplicationContext 完全初始化开始工作之后,
callRunners()
可以看出是 run 方法内部最后一个调用的方法(可以认为是 main 方法执行完成之前最后一步) - 只要存在于当前 SpringBoot 应用的 ApplicationContext 中的任何 CommandLineRunner/ApplicationRunner,都会被加载执行(不管你是手动注册还是自动扫描去 Ioc 容器)
方法 3:spring 事件机制
Spring 的事件机制实际上是设计模式中观察者模式的典型应用。观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态改变时,需要通知相应的观察者,使这些观察者能够自动更新。
Spring 的事件驱动模型由三部分组成
- 事件:
ApplicationEvent
,继承自 JDK 的EventObject
,所有事件都要继承它,也就是被观察者 - 事件发布者:
ApplicationEventPublisher
及ApplicationEventMulticaster
接口,使用这个接口,就可以发布事件了 - 事件监听者::
ApplicationListener
,继承 JDK 的EventListener
,所有监听者都继承它,也就是我们所说的观察者,当然我们也可以使用注解@EventListener
,效果是一样的
built-in 事件
在 Spring 框架中,内置了 4 种事件:
- ContextStartedEvent:ApplicationContext 启动后触发的事件
- ContextStoppedEvent:ApplicationContext 停止后触发的事件
- ContextRefreshedEvent:ApplicationContext 初始化或刷新完成后触发的事件;(容器初始化完成后调用,所以我们可以利用这个事件做一些初始化操作)
- ContextClosedEvent:ApplicationContext 关闭后触发的事件;(如 web 容器关闭时自动会触发 spring 容器的关闭,如果是普通 java 应用,需要调用 ctx.registerShutdownHook(); 注册虚拟机关闭时的钩子才行)
例子
以 ContextRefreshedEvent
为例,创建一个 listener 非常简单
@Component
@Slf4j
public class MyContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {log.info("ContextRefreshedEvent listen...");
}
}
MyContextRefreshListener
监听了内置事件 ContextRefreshedEvent
,即容器初始化完成后调用,MyContextRefreshListener.onApplicationEvent
会被调用,利用此特性可以做一些初始化工作
注意: 在传统的基于 XML 配置的 Spring 项目中会存在二次调用的问题,即调用两次该方法,原因是在传统的 Spring MVC 项目中,系统存在两个容器,一个 root 容器,一个 project-servlet.xml 对应的子容器,在初始化这两个容器的时候都会调用该方法一次,所以有二次调用的问题,而对于基于 Springboot 的项目不存在这个问题
三种方式的执行顺序
方法 1(spring bean 初始化)–> spring 事件ContextRefreshedEvent
–> CommandLineRunner/ApplicationRunner,示例代码下载请戳代码下载地址
参考文档
- Spring Bean 初始化之 InitializingBean, init-method 和 PostConstruct
- SpringBoot 中资源初始化加载的几种方式(看这一片就够了)
- springboot 系列文章之启动时初始化数据
- SpringBoot 启动初始化资源