7

摘要

在实际项目中,我们需要在springboot服务启动后做一些初始化工作,例如线程池初始化、文件资源加载、常驻后台任务启动(比如kafka consumer)等。本文介绍3类初始化资源的方法:

  • Spring Bean初始化的InitializingBean,init-method和PostConstruct
  • ApplicationRunnerCommandLineRunner接口
  • 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()之前执行,非常适合在应用程序启动之初进行一些数据初始化的工作。CommandLineRunnerApplicationRunner这两个接口工作方式相同,都只提供单一的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,所有事件都要继承它,也就是被观察者
  • 事件发布者: ApplicationEventPublisherApplicationEventMulticaster接口,使用这个接口,就可以发布事件了
  • 事件监听者::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,示例代码下载请戳代码下载地址

参考文档


skyarthur
1.6k 声望1.3k 粉丝

技术支持业务,技术增强业务,技术驱动业务