• Spring Boot 如何在启动后执行初始化任务(转)


    原文:https://zhuanlan.zhihu.com/p/352967633

    作者:神马翔

    Spring 应用有时会在应用启动后做一些初始化的操作,比如从数据库中拉取一些数据缓存起来,比如读取一些配置变量。如何在容器启动后来执行一个任务呢?本文针对这个问题,探讨一下几个方面的内容。

    1. Spring 是如何监听启动事件的?
    2. Spring Boot 中的 ApplicationRunner 和 CommandLineRunner 是什么?
    3. ApplicationRunner 和 CommandLineRunner 的区别。

    监听 ContextRefreshedEvent

    如果要在容器启动后做一些操作,第一直觉就是使用监听器监听容器的启动事件,在回调函数中完成任务。Spring 中我们也是这么做的。通过监听 ContextRefreshedEvent(该事件发生在容器初始化完毕后)实现自定义的初始化逻辑。

    @Component
    public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
            System.out.println("容器初始化完毕");
        }
    }

    以上代码能生效的原因是,Spring 在初始化 ApplicationContext 的时候,会从当前的 bean 中找到 ApplicationListener 类型的 bean,将这些 bean 注册到 ApplicationContext 的事件发布器上。

    protected void registerListeners() {
      ...
      String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
      for (String listenerBeanName : listenerBeanNames) {
        getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
      }
      ..
    }

    ContextRefreshedEvent 事件是 ApplicationContextEvent 的一个子类,ApplicationContextEvent 的子类有很多,分别表示了 ApplicationContext 生命周期的不同阶段。ContextRefreshedEvent 事件发生在容器初始化完毕后。此时 Spring 已经将所有的 bean 被成功加载,我们可以在这个监听器中注入我们要用到的 bean,就像写正常的业务代码一样,完成启动后的初始化任务。

    监听 ContextRefreshedEvent 事件的方式在 Spring 和 Spring Boot 中都行的通。不仅如此,我们还可以监听各种的 ApplicationContextEvent,比如监听 ContextStoppedEvent,用于容器销毁是删除一些副作用。

    ApplicationRunner 和 CommandLineRunner 的用法

    除了监听事件外,Spring Boot 其实还提供了两个接口,专门用于完成启动后的初始化工作,那就是 ApplicationRunner 和 CommandLineRunner。这两个接口的用法是一样的,继承后实现 run 方法,这个 run 方法会在容器初始化完毕后执行。它们还实现了 Order 接口,可以自定义执行顺序。

    @Order(1)
    @Component
    public class AppStartupRunner implements ApplicationRunner {
        @Override
        public void run(ApplicationArguments args) throws Exception {
            System.out.println("初始化代码");
        }
    }

    Spring Boot 这么设计,其实是为了概念上将 Context 事件和应用初始化做分隔,因为在 ContextRefreshedEvent 事件发生的时候,只是 bean 的上下文环境配置好了,并这并不是容器启动的最后一步,后续还有一些行为,比如 SpringApplicationRunListener 会发出事件等。我们监听 ContextRefreshedEvent 事件,能实现执行初始化任务的目标,但在语义上两者是不一致的。

    ApplicationRunner 和 CommandLineRunner 是 Spring Boot 提供的专门用于处理启动后的初始化工作的接口,他们的执行一定是在容器启动的最后一步。也就是 run 方法的最后一步。

    public ConfigurableApplicationContext run(String... args) {
            ...
            try {
                ...
                callRunners(context, applicationArguments);
            }
            ...
        }

    callRunners 中就是对 ApplicationRunner 和 CommandLineRunner 类的调用,Spring 从当前的 bean 集合中拿出类型为 ApplicationRunner 和 CommandLineRunner 的实例,将其放到一个列表中,然后根据 order 申明排序,依次执行 bean 的 run 方法。

    private void callRunners(ApplicationContext context, ApplicationArguments args) {
      List<Object> runners = new ArrayList<>();
      runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
      runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
      // 排序
      AnnotationAwareOrderComparator.sort(runners);
      // 执行 run 方法
      for (Object runner : new LinkedHashSet<>(runners)) {
        if (runner instanceof ApplicationRunner) {
          callRunner((ApplicationRunner) runner, args);
        }
        if (runner instanceof CommandLineRunner) {
          callRunner((CommandLineRunner) runner, args);
        }
      }
    }

    从代码中可以看出,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 对参数进行了简单处理,而后者获得参数经过切分的数组。
  • 相关阅读:
    Java面向对象(继承、抽象类)
    Java面向对象(类、封装)
    Java基础语法(Eclipse)
    JavaScript new对象的四个过程
    原生js实现深复制
    es6 实现双链表
    快速排序
    跨域问题
    pm2 使用
    js冒泡排序
  • 原文地址:https://www.cnblogs.com/ajianbeyourself/p/15004712.html
Copyright © 2020-2023  润新知