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接口即可,非常优雅。“