摘要
在实际项目中,我们需要在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,示例代码下载请戳代码下载地址