文章目录

前言

相信熟悉使用SpringBoot开发的人一定没少用过ApplicationRunner(至少我是没少用),一般都是通过实现ApplicationRunner接口+覆写run方法来嵌入一些操作至springboot应用启动完成阶段。

但其实除了ApplicationRunner的实现,CommandLineRunner的实现跟ApplicationRunner是在同一阶段调用的,那么他们到底有什么不同呢?虽然通过一些简单的实验能得出结论,但既然来都来了,不妨跟进下源码瞧瞧。

SpringBoot Version:2.1.7

实验代码

以下给出部分实验代码:

FirstApplicationRunner.java

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(1)
public class FirstApplicationRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("\u001B[32m >>> startup first application runner<<<");
    }
}

FirstCommandlineRunner.java

@Component
@Order(1)
public class FirstCommandlineRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("\u001B[32m >>> startup first 
command runner<<<");
    }
}

SecondApplicationRunner.java

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(2)
public class SecondApplicationRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("\u001B[32m >>> startup second application runner<<<");
    }
}


SecondCommandlineRunner.java

import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(2)
public class SecondCommandlineRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("\u001B[32m >>> startup second 
command runner<<<");
    }
}

部分输出结果:

 >>> startup first application runner<<<
 >>> startup first command runner<<<
 >>> startup second application runner<<<
 >>> startup second command runner<<<

如果将上述实验代码的@Order都去掉,顺序会发生神奇的调换:

 >>> startup first application runner<<<
 >>> startup second application runner<<<
 >>> startup first command runner<<<
 >>> startup second command runner<<<
  • 不再是交替执行,而是ApplicationRunner优先。

跟进源码

开始跟进

直接跟进SpringApplication#run方法
org.springframework.boot.SpringApplication#run(java.lang.String...)

	public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
                        
			// 跟进这里
			callRunners(context, applicationArguments);
                        ...(略)

跟进callRunners方法,另外,请上面的run方法里构造了ApplicationArguments,后面还会分析它
org.springframework.boot.SpringApplication#callRunners

	private void callRunners(ApplicationContext context, ApplicationArguments args) {
		List<Object> runners = new ArrayList<>();
		// 获取ApplicationRunner的实现
		runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
		// 获取CommandLineRunner的实现
		runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
		// 根据@Order的值排序
		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);
			}
		}
	}
  • 在没有用@Order设置顺序时,永远是优先获取ApplicationRunner的实现,故ApplicationRunner会被优先回调。

这里先跟进getBeansOfType,这个方法没什么特别之处,只是遇到了之前学过的东西,想回顾一下,不喜欢的可以跳过下面两段:
org.springframework.context.support.AbstractApplicationContext#getBeansOfType(java.lang.Class<T>)

	@Override
	public <T> Map<String, T> getBeansOfType(@Nullable Class<T> type) throws BeansException {
		assertBeanFactoryActive();
		return getBeanFactory().getBeansOfType(type);
	}

跟进assertBeanFactoryActive方法:
org.springframework.context.support.AbstractApplicationContext#assertBeanFactoryActive

	protected void assertBeanFactoryActive() {
		if (!this.active.get()) {
			if (this.closed.get()) {
				throw new IllegalStateException(getDisplayName() + " has been closed already");
			}
			else {
				throw new IllegalStateException(getDisplayName() + " has not been refreshed yet");
			}
		}
	}
  • 这里的active和closed,在介绍preparerefresh的时候说过。

好了,将视角重新返回callRunners方法,来直接看看两种callRunner的重载:

	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);
		}
	}

可见代码就是普通的重载,我们可以来看看断点的调试结果,下面拿FirstApplicationRunner做例子:

image.png
继续跟进
image.png

再确保代码确实是如我们所想的那般运行后,我们将注意力重新放回两个callRunner方法上,我们能看到两种实现主要区别在run方法上:

// application
(runner).run(args);
(runner).run(args.getSourceArgs());

那么args和args.getSourceArgs()到底有什么区别呢?可以说整个流程下来也就这两参数的不同就直接代表了ApplicationRunner和CommandLineRunner的不同了。为了解答这个问题,我们需要回到前面的代码。

ApplicationRunner和CommandLineRunner的区别

视角重新回到SpringApplication#run方法,找到如下一行代码(前面也特别提醒过了):
org.springframework.boot.SpringApplication#run(java.lang.String...)

ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
  • 前面callRunner方法传给run的args对象其实就是此处的applicationArguments。

跟进构造方法:
org.springframework.boot.DefaultApplicationArguments#DefaultApplicationArguments

	public DefaultApplicationArguments(String[] args) {
		Assert.notNull(args, "Args must not be null");
		this.source = new Source(args);
		this.args = args;
	}
  • CommandLineRunner#run仅仅传进了source。

这里我们应该关注Source到底是什么,持续跟进构造函数,直到来到如下代码:
org.springframework.core.env.SimpleCommandLinePropertySource#SimpleCommandLinePropertySource(java.lang.String...)

	public SimpleCommandLinePropertySource(String... args) {
		super(new SimpleCommandLineArgsParser().parse(args));
	}

这里的parse方法就是关键,跟进它:
org.springframework.core.env.SimpleCommandLineArgsParser#parse

	public CommandLineArgs parse(String... args) {
		CommandLineArgs commandLineArgs = new CommandLineArgs();
		for (String arg : args) {
			if (arg.startsWith("--")) {
				String optionText = arg.substring(2, arg.length());
				String optionName;
				String optionValue = null;
				if (optionText.contains("=")) {
					optionName = optionText.substring(0, optionText.indexOf('='));
					optionValue = optionText.substring(optionText.indexOf('=')+1, optionText.length());
				}
				else {
					optionName = optionText;
				}
				if (optionName.isEmpty() || (optionValue != null && optionValue.isEmpty())) {
					throw new IllegalArgumentException("Invalid argument syntax: " + arg);
				}
				commandLineArgs.addOptionArg(optionName, optionValue);
			}
			else {
				commandLineArgs.addNonOptionArg(arg);
			}
		}
		return commandLineArgs;
	}
  • 这段代码的逻辑其实很简单,就是截取参数--后面的key=value。

如果你还不明白的话,下面通过修改一些参数+断点来直观感受下:

增加启动参数--number=7
image.png

断点至FirstCommandlineRunner#run:
FirstCommandlineRunner#run.png

断点至FirstCommandlineRunner#run
image.png

小结

  • 无论是ApplicationRunner还是CommandLineRunner的实现,都会在SpringApplication启动完成之后(running事件广播之前)被回调。
  • 两种实现在没有设置@Order时,ApplicationRunner实现的回调有绝对的优先权(绝对优于CommandLineRunner)。
  • ApplicationRunner相当于是CommandLineRunner的增强版。