一张图帮你记忆,Spring Boot 应用在启动阶段执行代码的几种方式

前言

有时候咱们须要在应用启动时执行一些代码片断,这些片断多是仅仅是为了记录 log,也多是在启动时检查与安装证书 ,诸如上述业务要求咱们可能会常常碰到html

Spring Boot 提供了至少 5 种方式用于在应用启动时执行代码。咱们应该如何选择?本文将会逐步解释与分析这几种不一样方式java


CommandLineRunner

CommandLineRunner 是一个接口,经过实现它,咱们能够在 Spring 应用成功启动以后 执行一些代码片断spring

@Slf4j
@Component
@Order(2)
public class MyCommandLineRunner implements CommandLineRunner {
    
    @Override
    public void run(String... args) throws Exception {
        log.info("MyCommandLineRunner order is 2");
        if (args.length > 0){
            for (int i = 0; i < args.length; i++) {
                log.info("MyCommandLineRunner current parameter is: {}", args[i]);
            }
        }
    }
}

当 Spring Boot 在应用上下文中找到 CommandLineRunner bean,它将会在应用成功启动以后调用 run() 方法,并传递用于启动应用程序的命令行参数segmentfault

经过以下 maven 命令生成 jar 包:数组

mvn clean package

经过终端命令启动应用,并传递参数:安全

java -jar springboot-application-startup-0.0.1-SNAPSHOT.jar --foo=bar --name=rgyb

查看运行结果:
springboot

到这里咱们能够看出几个问题:bash

  1. 命令行传入的参数并无被解析,而只是显示出咱们传入的字符串内容 --foo=bar--name=rgyb,咱们能够经过 ApplicationRunner 解析,咱们稍后看
  2. 在重写的 run() 方法上有 throws Exception 标记,Spring Boot 会将 CommandLineRunner 做为应用启动的一部分,若是运行 run() 方法时抛出 Exception,应用将会终止启动
  3. 咱们在类上添加了 @Order(2) 注解,当有多个 CommandLineRunner 时,将会按照 @Order 注解中的数字从小到大排序 (数字固然也能够用复数)

⚠️不要使用 @Order 太多

看到 order 这个 "黑科技" 咱们会以为它能够很是方便将启动逻辑按照指定顺序执行,但若是你这么写,说明多个代码片断是有相互依赖关系的,为了让咱们的代码更好维护,咱们应该减小这种依赖使用app

小结

若是咱们只是想简单的获取以空格分隔的命令行参数,那 MyCommandLineRunner 就足够使用了maven

ApplicationRunner

上面提到,经过命令行启动并传递参数,MyCommandLineRunner 不能解析参数,若是要解析参数,那咱们就要用到 ApplicationRunner 参数了

@Component
@Slf4j
@Order(1)
public class MyApplicationRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("MyApplicationRunner order is 1");
        log.info("MyApplicationRunner Current parameter is {}:", args.getOptionValues("foo"));
    }
}

从新打 jar 包,运行以下命令:

java -jar springboot-application-startup-0.0.1-SNAPSHOT.jar --foo=bar,rgyb

运行结果以下:

到这里咱们能够看出:

  1. MyCommandLineRunner 类似,但 ApplicationRunner 能够经过 run 方法的 ApplicationArguments 对象解析出命令行参数,而且每一个参数能够有多个值在里面,由于 getOptionValues 方法返回 List<String> 数组
  2. 在重写的 run() 方法上有 throws Exception 标记,Spring Boot 会将 CommandLineRunner 做为应用启动的一部分,若是运行 run() 方法时抛出 Exception,应用将会终止启动
  3. ApplicationRunner 也可使用 @Order 注解进行排序,从启动结果来看,它与 CommandLineRunner 共享 order 的顺序,稍后咱们经过源码来验证这个结论

小结

若是咱们想获取复杂的命令行参数时,咱们可使用 ApplicationRunner

ApplicationListener

若是咱们不须要获取命令行参数时,咱们能够将启动逻辑绑定到 Spring 的 ApplicationReadyEvent

@Slf4j
@Component
@Order(0)
public class MyApplicationListener implements ApplicationListener<ApplicationReadyEvent> {

    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        log.info("MyApplicationListener is started up");
    }
}

运行程序查看结果:

到这咱们能够看出:

  1. ApplicationReadyEvent 当且仅当 在应用程序就绪以后才被触发,甚至是说上面的 Listener 要在本文说的全部解决方案都执行了以后才会被触发,最终结论请稍后看
  2. 代码中我用 Order(0) 来标记,显然 ApplicationListener 也是能够用该注解进行排序的,按数字大小排序,应该是最早执行。可是,这个顺序仅用于同类型的 ApplicationListener 之间的排序,与前面提到的 ApplicationRunnersCommandLineRunners 的排序并不共享

小结

若是咱们不须要获取命令行参数,咱们能够经过 ApplicationListener<ApplicationReadyEvent> 建立一些全局的启动逻辑,咱们还能够经过它获取 Spring Boot 支持的 configuration properties 环境变量参数

若是你看过我以前写的 Spring Bean 生命周期三部曲:

那么你会对下面两种方式很是熟悉了

@PostConstruct

建立启动逻辑的另外一种简单解决方案是提供一种在 bean 建立期间由 Spring 调用的初始化方法。咱们要作的就只是将 @PostConstruct 注解添加到方法中:

@Component
@Slf4j
@DependsOn("myApplicationListener")
public class MyPostConstructBean {

    @PostConstruct
    public void testPostConstruct(){
        log.info("MyPostConstructBean");
    }
}

查看运行结果:

从上面运行结果能够看出:

  1. Spring 建立完 bean以后 (在启动以前),便会当即调用 @PostConstruct 注解标记的方法,所以咱们没法使用 @Order 注解对其进行自由排序,由于它可能依赖于 @Autowired 插入到咱们 bean 中的其余 Spring bean。
  2. 相反,它将在依赖于它的全部 bean 被初始化以后被调用,若是要添加人为的依赖关系并由此建立一个排序,则可使用 @DependsOn 注解(虽然能够排序,可是不建议使用,理由和 @Order 同样)

小结

@PostConstruct 方法固有地绑定到现有的 Spring bean,所以应仅将其用于此单个 bean 的初始化逻辑;

InitializingBean

@PostConstruct 解决方案很是类似,咱们能够实现 InitializingBean 接口,并让 Spring 调用某个初始化方法:

@Component
@Slf4j
public class MyInitializingBean implements InitializingBean {


    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("MyInitializingBean.afterPropertiesSet()");
    }
}

查看运行结果:

从上面的运行结果中,咱们获得了和 @PostConstruct 同样的效果,但两者仍是有差异的

⚠️ @PostConstructafterPropertiesSet 区别

  1. afterPropertiesSet,顾名思义「在属性设置以后」,调用该方法时,该 bean 的全部属性已经被 Spring 填充。若是咱们在某些属性上使用 @Autowired(常规操做应该使用构造函数注入),那么 Spring 将在调用afterPropertiesSet 以前将 bean 注入这些属性。但 @PostConstruct 并无这些属性填充限制
  2. 因此 InitializingBean.afterPropertiesSet 解决方案比使用 @PostConstruct 更安全,由于若是咱们依赖还没有自动注入的 @Autowired 字段,则 @PostConstruct 方法可能会遇到 NullPointerExceptions

小结

若是咱们使用构造函数注入,则这两种解决方案都是等效的

源码分析

请打开你的 IDE (重点代码已标记注释):

MyCommandLineRunnerApplicationRunner 是在什么时候被调用的呢?

打开 SpringApplication.java 类,里面有 callRunners 方法

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList<>();
    //从上下文获取 ApplicationRunner 类型的 bean
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());

    //从上下文获取 CommandLineRunner 类型的 bean
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());

    //对两者进行排序,这也就是为何两者的 order 是能够共享的了
    AnnotationAwareOrderComparator.sort(runners);

    //遍历对其进行调用
    for (Object runner : new LinkedHashSet<>(runners)) {
        if (runner instanceof ApplicationRunner) {
            callRunner((ApplicationRunner) runner, args);
        }
        if (runner instanceof CommandLineRunner) {
            callRunner((CommandLineRunner) runner, args);
        }
    }
}

强烈建议完整看一下 SpringApplication.java 的所有代码,Spring Boot 启动过程及原理均可以从这个类中找到一些答案

总结

最后画一张图用来总结这几种方式(高清大图请查看原文:https://dayarch.top/p/spring-...

灵魂追问

  1. 上面程序运行结果, afterPropertiesSet 方法调用先于 @PostConstruct 方法,但这和咱们在 Spring Bean 生命周期之缘起 中的调用顺序偏偏相反,你知道为何吗?
  2. MyPostConstructBean 经过 @DependsOn("myApplicationListener") 依赖了 MyApplicationListener,为何调用结果前者先与后者呢?
  3. 为何不建议 @Autowired 形式依赖注入

在写 Spring Bean 生命周期时就有朋友问我与之相关的问题,显然他们在概念上有一些含混,因此,仔细理解上面的问题将会帮助你加深对 Spring Bean 生命周期的理解

欢迎关注个人公众号 「日拱一兵」,趣味原创解析Java技术栈问题,将复杂问题简单化,将抽象问题图形化落地
若是对个人专题内容感兴趣,或抢先看更多内容,欢迎访问个人博客 dayarch.top
相关文章
相关标签/搜索