SpringBoot 之 ApplicationRunner、CommandLineRunner

SpringBoot 之 ApplicationRunner、CommandLineRunner

 

 

1、简介

Spring启动时,容器刷新完成之后,提供了扩展接口CommandLineRunner或者ApplicationRunner, 执行最后的逻辑。SpringApplication在启动完成后,会执行一次所有实现了这些接口类的run方法;CommandLineRunner和ApplicationRunner的作用是相同的。不同之处在于CommandLineRunner接口的run()方法接收String数组作为参数,即是最原始的参数,没有做任何处理;而ApplicationRunner接口的run()方法接收ApplicationArguments对象作为参数,是对原始参数做了进一步的封装。在开发过程中会有这样的场景:需要在容器启动的时候执行一些内容,比如:读取配置文件信息,数据库连接,删除临时文件,清除缓存信息,在Spring框架下是通过ApplicationListener监听器来实现的。在Spring Boot中给我们提供了两个接口CommandLineRunner和ApplicationRunner,来帮助我们实现这样的需求。

ApplicationRunner
public interface ApplicationRunner {
    void run(ApplicationArguments args) throws Exception;
}
CommandLineRunner 
public interface CommandLineRunner {
    void run(String... args) throws Exception;
}

 

2、使用场景

在所有的CommandLineRunner和ApplicationRunner回调之前,下面的步骤已经确保执行完毕:

1、Environment内置变量的创建和属性填充已经完成。

2、Banner已经打印完毕。

3、ApplicationContext和BeanFactory创建完成,并且完成了上下文刷新(refreshContext),意味着所有单例的Bean完成了初始化以及属性装配。

4、Servlet容器启动成功,如内置的Tomcat、Jetty容器已经正常启动,可以正常接收请求和处理。

5、启动信息完成打印,一般会看到日志输出类似Started ***Application in *** seconds (JVM running for ***)。

应用程序启动后,需要执行特定的代码,比如:加载缓存数据、打印自定义启动信息等。Spring Boot 为我们提供了ApplicationRunner、CommandLineRunner两个接口来实现上面的需求。

应用服务启动时,加载一些数据和执行一些应用的初始化动作。举例说明: (1)删除临时文件。 (2)缓存预热:项目启动时热加载数据库数据至缓存。 (3)清除缓存信息。 (4)读取配置文件信息。 (5)打印日志用于标识服务启动成功或者标识某些属性加载成功。 (6)设置属性值或者启动组件,例如开启某些组件的开关、一些应用级别缓存的加载、启动定时任务等等。 (7)需要使用main方法的入参。

 

3、使用示例

@Order(1)
@Component
public class PrintArgsService implements CommandLineRunner{
    @Override
    public void run(String... args) throws Exception {
        System.out.println("=====应用已经成功启动====="+ Arrays.asList(args));
    }
}

可以在实现类加上@Order注解指定执行的顺序,数字越小,优先级越高,也就是@Order(1)注解的类会在@Order(2)注解的类之前执行。

或者在命令行或者启动参数指定相关参数使用:

 

4、源码解读

顺着SpringApplication.run(FanfuApplication.class, args)进入到run(String... args)中,CommandLineRunner和ApplicationRunner的执行入口就在这里,之前在其他分享其他扩展点时,经常遇到的AbstractApplicationContext#refresh(),其实是第25行 refreshContext(context)中触发的。

public ConfigurableApplicationContext run(String... args) {
    //springboot启动前的准备工作
   StopWatch stopWatch = new StopWatch();
   stopWatch.start();
   ConfigurableApplicationContext context = null;
   Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
   configureHeadlessProperty();
   //启动要开始的时候触发了SpringApplicationRunListeners
   SpringApplicationRunListeners listeners = getRunListeners(args);
   listeners.starting();
   try {
       //启动参数args包装
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
      //准备系统环境
      ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
      configureIgnoreBeanInfo(environment);
      //打印启动时标志图形
      Banner printedBanner = printBanner(environment);
      //创建Spring的上下文环境
      context = createApplicationContext();
      exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
            new Class[] { ConfigurableApplicationContext.class }, context);
      prepareContext(context, environment, listeners, applicationArguments, printedBanner);
      //刷新Spring容器
      refreshContext(context);
      //容器启动后的一些后置处理
      afterRefresh(context, applicationArguments);
      stopWatch.stop();
      if (this.logStartupInfo) {
         new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
      }
      //触发了Spring容器启动完成的事件
      listeners.started(context);
      //开始调用CommandLineRunner和ApplicationRunner的扩展点方法
      callRunners(context, applicationArguments);
   }
   catch (Throwable ex) {
      handleRunFailure(context, ex, exceptionReporters, listeners);
      throw new IllegalStateException(ex);
   }
 
   try {
      listeners.running(context);
   }
   catch (Throwable ex) {
      handleRunFailure(context, ex, exceptionReporters, null);
      throw new IllegalStateException(ex);
   }
   return context;
}

CommandLineRunner和ApplicationRunner的扩展点方法的调用逻辑,其实也是简单易懂,先把所有CommandLineRunner和ApplicationRunner的实现类汇总到一个集合,然后循环遍历这个集合,在集合里判断,如果ApplicationRunner的实现类,则先执行;如果是CommandLineRunner的实现类,则后执行;非常的朴实无华。

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    //汇总CommandLineRunner和ApplicationRunner的实现类到runners集合
   List<Object> runners = new ArrayList<>();
   runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
   runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
   AnnotationAwareOrderComparator.sort(runners);
   //循环遍历runners 集合
   for (Object runner : new LinkedHashSet<>(runners)) {
       //如果ApplicationRunner的实现类,则先执行
      if (runner instanceof ApplicationRunner) {
         callRunner((ApplicationRunner) runner, args);
      }
      //如果是CommandLineRunner的实现类,则后执行;
      if (runner instanceof CommandLineRunner) {
         callRunner((CommandLineRunner) runner, args);
      }
   }
}
private void callRunner(ApplicationRunner runner, ApplicationArguments args) {
   try {
      (runner).run(args);
   }
   catch (Exception ex) {
      throw new IllegalStateException("Failed to execute ApplicationRunner", ex);
   }
}
private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
   try {
      (runner).run(args.getSourceArgs());
   }
   catch (Exception ex) {
      throw new IllegalStateException("Failed to execute CommandLineRunner", ex);
   }
}

 

总结
如果面试过程中,有面试官这样问你:”对业务上一些热点数据需要在项目启动前进行预加载,你有什么好的办法吗?“

你可以这么回答他:”我了解Springboot有两个扩展点:CommandLineRunner和ApplicationRunner,其触发执行时机是在Spring容器、Tomcat容器正式启动完成后,可以正式处理业务请求前,刚好可以做一些热点数据预先加载完全可以使用这个方法,实现方式也很简单,实现CommandLineRunner或ApplicationRunner接口即可,非常优雅。“

 

posted @ 2023-12-06 22:44  邓维-java  阅读(1910)  评论(0)    收藏  举报