SpringBoot CommandLineRunner

1、CommandLineRunner

SpringBoot中CommandLineRunner的作用

平常开发中有可能需要实现在项目启动后执行的功能,SpringBoot提供的一种简单的实现方案就是添加一个model并实现CommandLineRunner接口,实现功能的代码放在实现的run方法中。也就是项目一启动之后,就立即需要执行的动作。只需要在项目里面简单的配置,就可以实现这个功能。

简单例子

  1. import org.springframework.boot.CommandLineRunner;
  2. import org.springframework.stereotype.Component;
  3. @Component
  4. public class MyStartupRunner implements CommandLineRunner {
  5. @Override
  6. public void run(String... args) throws Exception {
  7. System.out.println("项目已经启动");
  8. }
  9. }

多个类实现CommandLineRunner接口执行顺序的保证

通过实现Ordered接口实现控制执行顺序

  1. import lombok.extern.slf4j.Slf4j;
  2. import org.springframework.boot.CommandLineRunner;
  3. import org.springframework.core.Ordered;
  4. import org.springframework.stereotype.Component;
  5. /**
  6. * 优先级最高
  7. * 该类期望在springboot 启动后第一顺位执行
  8. * @since 12:57
  9. **/
  10. @Slf4j
  11. @Component
  12. public class HighOrderCommandLineRunner implements CommandLineRunner, Ordered {
  13. @Override
  14. public void run(String... args) throws Exception {
  15. for (String arg : args) {
  16. log.info("arg = " + arg);
  17. }
  18. log.info("i am highOrderRunner");
  19. }
  20. @Override
  21. public int getOrder() {
  22. return Integer.MIN_VALUE+1;
  23. }
  24. }
  1. import lombok.extern.slf4j.Slf4j;
  2. import org.springframework.boot.CommandLineRunner;
  3. import org.springframework.core.Ordered;
  4. import org.springframework.stereotype.Component;
  5. /**
  6. * 优先级低于{@code HighOrderCommandLineRunner}
  7. * @since 12:59
  8. **/
  9. @Slf4j
  10. @Component
  11. public class LowOrderCommandLineRunner implements CommandLineRunner, Ordered {
  12. @Override
  13. public void run(String... args) throws Exception {
  14. log.info("i am lowOrderRunner");
  15. }
  16. @Override
  17. public int getOrder() {
  18. return Integer.MIN_VALUE+1;
  19. }
  20. }

启动Spring Boot程序后,控制台按照预定的顺序打印出了结果:

  1. 2020-05-30 23:11:03.685 INFO 11976 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
  2. 2020-05-30 23:11:03.701 INFO 11976 --- [ main] c.f.Application : Started SpringBootApplication in 4.272 seconds (JVM running for 6.316)
  3. 2020-05-30 23:11:03.706 INFO 11976 --- [ main] c.f.HighOrderCommandLineRunner : i am highOrderRunner
  4. 2020-05-30 23:11:03.706 INFO 11976 --- [ main] c.f.LowOrderCommandLineRunner : i am lowOrderRunner

通过@Order注解实现控制执行顺序

SpringBoot在项目启动后会遍历所有实现CommandLineRunner的实体类并执行run方法,如果需要按照一定的顺序去执行,那么就需要在实体类上使用一个@Order注解(或者实现Order接口)来表明顺序

  1. import org.springframework.boot.CommandLineRunner;
  2. import org.springframework.core.annotation.Order;
  3. import org.springframework.stereotype.Component;
  4. @Component
  5. @Order(value=2)
  6. public class MyStartupRunner1 implements CommandLineRunner {
  7. @Override
  8. public void run(String... args) throws Exception {
  9. System.out.println("执行2");
  10. }
  11. }
  1. import org.springframework.boot.CommandLineRunner;
  2. import org.springframework.core.annotation.Order;
  3. import org.springframework.stereotype.Component;
  4. @Component
  5. @Order(value=1)
  6. public class MyStartupRunner2 implements CommandLineRunner {
  7. @Override
  8. public void run(String... args) throws Exception {
  9. System.out.println("执行1");
  10. }
  11. }

控制台显示

  1. 执行1
  2. 执行2

根据控制台结果可判断,@Order 注解的执行优先级是按value值从小到大顺序。

@Order 作用

项目启动之后,要执行的动作是比较的多,那么到底先执行哪个,那么就可以利用这个注解限定优先级。 :::danger Ordered接口并不能被 @Order注解所代替。 :::

2、ApplicationRunner

在Spring Boot 1.3.0又引入了一个和CommandLineRunner功能一样的接口ApplicationRunnerCommandLineRunner接收可变参数String... args,而ApplicationRunner 接收一个封装好的对象参数ApplicationArguments。除此之外它们功能完全一样,甚至连方法名都一样。声明一个ApplicationRunner并让它优先级最低:

  1. import lombok.extern.slf4j.Slf4j;
  2. import org.springframework.boot.ApplicationArguments;
  3. import org.springframework.boot.ApplicationRunner;
  4. import org.springframework.core.Ordered;
  5. import org.springframework.stereotype.Component;
  6. import java.util.Arrays;
  7. import java.util.List;
  8. import java.util.Set;
  9. /**
  10. * 优先级最低
  11. **/
  12. @Slf4j
  13. @Component
  14. public class DefaultApplicationRunner implements ApplicationRunner, Ordered {
  15. @Override
  16. public void run(ApplicationArguments args) throws Exception {
  17. log.info("i am applicationRunner");
  18. Set<String> optionNames = args.getOptionNames();
  19. log.info("optionNames = " + optionNames);
  20. String[] sourceArgs = args.getSourceArgs();
  21. log.info("sourceArgs = " + Arrays.toString(sourceArgs));
  22. List<String> nonOptionArgs = args.getNonOptionArgs();
  23. log.info("nonOptionArgs = " + nonOptionArgs);
  24. List<String> optionValues = args.getOptionValues("foo");
  25. log.info("optionValues = " + optionValues);
  26. }
  27. @Override
  28. public int getOrder() {
  29. return Integer.MIN_VALUE+2;
  30. }
  31. }

按照顺序打印了三个类的执行结果:

  1. 2020-06-01 13:02:39.420 INFO 19032 --- [ main] c.f.MybatisResultmapApplication : Started MybatisResultmapApplication in 1.801 seconds (JVM running for 2.266)
  2. 2020-06-01 13:02:39.423 INFO 19032 --- [ main] c.f.HighOrderCommandLineRunner : i am highOrderRunner
  3. 2020-06-01 13:02:39.423 INFO 19032 --- [ main] c.f.LowOrderCommandLineRunner : i am lowOrderRunner
  4. 2020-06-01 13:02:39.423 INFO 19032 --- [ main] c.f.DefaultApplicationRunner : i am applicationRunner
  5. 2020-06-01 13:02:39.423 INFO 19032 --- [ main] c.f.DefaultApplicationRunner : optionNames = []
  6. 2020-06-01 13:02:39.423 INFO 19032 --- [ main] c.f.DefaultApplicationRunner : sourceArgs = []
  7. 2020-06-01 13:02:39.423 INFO 19032 --- [ main] c.f.DefaultApplicationRunner : nonOptionArgs = []
  8. 2020-06-01 13:02:39.423 INFO 19032 --- [ main] c.f.DefaultApplicationRunner : optionValues = null

Ordered接口并不能被 @Order注解所代替。

3、传递参数

Spring Boot应用启动时是可以接受参数的,换句话说也就是Spring Bootmain方法是可以接受参数的。这些参数通过命令行 java -jar yourapp.jar 来传递。CommandLineRunner会原封不动照单全收这些接口,这些参数也可以封装到ApplicationArguments对象中供ApplicationRunner调用。看一下ApplicationArguments的相关方法:

  • getSourceArgs() 被传递给应用程序的原始参数,返回这些参数的字符串数组。
  • getOptionNames() 获取选项名称的Set字符串集合。如 --spring.profiles.active=dev --debug 将返回["spring.profiles.active","debug"]
  • getOptionValues(String name) 通过名称来获取该名称对应的选项值。如--foo=bar --foo=baz 将返回["bar","baz"]
  • containsOption(String name) 用来判断是否包含某个选项的名称。
  • getNonOptionArgs() 用来获取所有的无选项参数。

可以通过下面的命令运行一个 Spring Boot应用 Jar

  1. java -jar yourapp.jar --foo=bar --foo=baz --dev.name=fcant java fcantcn

或者在IDEA开发工具中打开Spring Boot应用main方法的配置项,进行命令行参数的配置,其他IDE工具同理。
运行Spring Boot应用,将会打印出:

  1. 2020-06-01 15:04:31.490 INFO 13208 --- [ main] c.f.HighOrderCommandLineRunner : arg = --foo=bar
  2. 2020-06-01 15:04:31.490 INFO 13208 --- [ main] c.f.HighOrderCommandLineRunner : arg = --foo=baz
  3. 2020-06-01 15:04:31.490 INFO 13208 --- [ main] c.f.HighOrderCommandLineRunner : arg = --dev.name=fcant
  4. 2020-06-01 15:04:31.490 INFO 13208 --- [ main] c.f.HighOrderCommandLineRunner : arg = java
  5. 2020-06-01 15:04:31.490 INFO 13208 --- [ main] c.f.HighOrderCommandLineRunner : arg = fcantcn
  6. 2020-06-01 15:04:31.491 INFO 13208 --- [ main] c.f.HighOrderCommandLineRunner : i am highOrderRunner
  7. 2020-06-01 15:04:31.491 INFO 13208 --- [ main] c.f.LowOrderCommandLineRunner : i am lowOrderRunner
  8. 2020-06-01 15:04:31.491 INFO 13208 --- [ main] c.f.DefaultApplicationRunner : i am applicationRunner
  9. 2020-06-01 15:04:31.491 INFO 13208 --- [ main] c.f.DefaultApplicationRunner : optionNames = [dev.name, foo]
  10. 2020-06-01 15:04:31.491 INFO 13208 --- [ main] c.f.DefaultApplicationRunner : sourceArgs = [--foo=bar, --foo=baz, --dev.name=fcant, java, fcantcn]
  11. 2020-06-01 15:04:31.491 INFO 13208 --- [ main] c.f.DefaultApplicationRunner : nonOptionArgs = [java, fcantcn]
  12. 2020-06-01 15:04:31.491 INFO 13208 --- [ main] c.f.DefaultApplicationRunner : optionValues = [bar, baz]

然后就可以根据实际需要动态地执行一些逻辑。

4、源码跟踪

通过源码理解一下底层实现。

run()方法

跟进run方法后,一路F6直达以下方法

  1. public ConfigurableApplicationContext run(String... args) {
  2. StopWatch stopWatch = new StopWatch();
  3. //设置线程启动计时器
  4. stopWatch.start();
  5. ConfigurableApplicationContext context = null;
  6. Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
  7. //配置系统属性:默认缺失外部显示屏等允许启动
  8. configureHeadlessProperty();
  9. //获取并启动事件监听器,如果项目中没有其他监听器,则默认只有EventPublishingRunListener
  10. SpringApplicationRunListeners listeners = getRunListeners(args);
  11. //将事件广播给listeners
  12. listeners.starting();
  13. try {
  14. //对于实现ApplicationRunner接口,用户设置ApplicationArguments参数进行封装
  15. ApplicationArguments applicationArguments = new DefaultApplicationArguments(
  16. args);
  17. //配置运行环境:例如激活应用***.yml配置文件
  18. ConfigurableEnvironment environment = prepareEnvironment(listeners,
  19. applicationArguments);
  20. configureIgnoreBeanInfo(environment);
  21. //加载配置的banner(gif,txt...),即控制台图样
  22. Banner printedBanner = printBanner(environment);
  23. //创建上下文对象,并实例化
  24. context = createApplicationContext();
  25. exceptionReporters = getSpringFactoriesInstances(
  26. SpringBootExceptionReporter.class,
  27. new Class[] { ConfigurableApplicationContext.class }, context);
  28. //配置SPring容器
  29. prepareContext(context, environment, listeners, applicationArguments,
  30. printedBanner);
  31. //刷新Spring上下文,创建bean过程中
  32. refreshContext(context);
  33. //空方法,子类实现
  34. afterRefresh(context, applicationArguments);
  35. //停止计时器:计算线程启动共用时间
  36. stopWatch.stop();
  37. if (this.logStartupInfo) {
  38. new StartupInfoLogger(this.mainApplicationClass)
  39. .logStarted(getApplicationLog(), stopWatch);
  40. }
  41. //停止事件监听器
  42. listeners.started(context);
  43. //开始加载资源
  44. callRunners(context, applicationArguments);
  45. }
  46. catch (Throwable ex) {
  47. handleRunFailure(context, listeners, exceptionReporters, ex);
  48. throw new IllegalStateException(ex);
  49. }
  50. listeners.running(context);
  51. return context;
  52. }

主要是熟悉SpringBoot的CommandLineRunner接口实现原理。因此上面SpringBoot启动过程方法不做过多介绍。直接进入CallRunners()方法内部。

callRunners方法

  1. private void callRunners(ApplicationContext context, ApplicationArguments args) {
  2. //将实现ApplicationRunner和CommandLineRunner接口的类,存储到集合中
  3. List<Object> runners = new ArrayList<>();
  4. runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
  5. runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
  6. //按照加载先后顺序排序
  7. AnnotationAwareOrderComparator.sort(runners);
  8. for (Object runner : new LinkedHashSet<>(runners)) {
  9. if (runner instanceof ApplicationRunner) {
  10. callRunner((ApplicationRunner) runner, args);
  11. }
  12. if (runner instanceof CommandLineRunner) {
  13. callRunner((CommandLineRunner) runner, args);
  14. }
  15. }
  16. }
  1. private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
  2. try {
  3. //调用各个实现类中的逻辑实现
  4. (runner).run(args.getSourceArgs());
  5. }
  6. catch (Exception ex) {
  7. throw new IllegalStateException("Failed to execute CommandLineRunner", ex);
  8. }
  9. }

到此结束,再跟进run()方法,就可以看到资源加载逻辑。