ApplicationRunner、CommandLineRunner 的区别(源码)
ApplicationRunner
、CommandLineRunner
都能在 springboot 启动时执行一些初始化的工作。javadoc 中说明了如果需要访问ApplicationArguments
而不是原始的String[]
需要使用ApplicationRunner
。两者run()
方法的参数不同,分别为ApplicationArguments
和String... args
。
做实验,看表象
@Slf4j
@Component
public class ApplicationRunnerTest implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
String[] sourceArgs = args.getSourceArgs();
log.info("sourceArgs: {}", Arrays.toString(sourceArgs));
List<String> nonOptionArgs = args.getNonOptionArgs();
log.info("nonOptionArgs: " + nonOptionArgs);
Set<String> optionNames = args.getOptionNames();
for (String optionName : optionNames) {
log.info("{}: {}", optionName, args.getOptionValues(optionName));
}
}
}
@Slf4j
@Component
public class CommandLineRunnerTest implements CommandLineRunner {
@Override
public void run(String... args) {
log.info("sourceArgs: {}", Arrays.toString(args));
}
}
启动时的参数为以下内容,其中有各种形式的参数,比如–xxx=yyy、–xxx、-xxx=yyy、-xxx、xxx=yyy、xxx
--name=tom --name=jerry --age=18 -height=50 weight=180 aaa --bbb
传给CommandLineRunner#run()
的参数没有进行任何处理。
传给ApplicationRunner#run()
的参加进行了处理,且 只处理了以 – 开头的参数,将其按照 = 分隔成了键值对,且相同键的放在了 List 中;其他的参数都放在了 nonOptionArgs 中。
... ApplicationRunnerTest : sourceArgs: [--name=tom, --name=jerry, --age=18, --aaa, -bbb=50, -ccc, ddd=180, eee]
... ApplicationRunnerTest : nonOptionArgs: [-bbb=50, -ccc, ddd=180, eee]
... ApplicationRunnerTest : aaa: []
... ApplicationRunnerTest : name: [tom, jerry]
... ApplicationRunnerTest : age: [18]
... CommandLineRunnerTest : sourceArgs: [--name=tom, --name=jerry, --age=18, --aaa, -bbb=50, -ccc, ddd=180, eee]
分析参数解析的源码
ApplicationRunner
被调用的地方是SpringApplication#callRunners()
方法。这里可以看到参数已经被解析好了,需要继续往上找。
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
// 找到所有 ApplicationRunner、CommandLineRunner 类型的 bean,放入 runners
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
// 排序
AnnotationAwareOrderComparator.sort(runners);
// 按上面的顺序执行
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
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);
}
}
SpringApplication#ConfigurableApplicationContext()
。在这里可以看出 Runner 几乎是在 springboot 项目启动的最后阶段执行的了,除了 Listener 之外。
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// ApplicationArguments 是在这里解析的
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, 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);
// SpringApplication#callRunners()
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
从DefaultApplicationArguments
的构造方法一直跟进,最后会找到类SimpleCommandLineArgsParser
,这是真正解析 args 参数的,只有一个方法parse()
。
从类的注释中可以看出,
- option arguments:只有符合
--optName[=optValue]
规则的才能被解析,必须以 “–” 开头,optValue 可以有也可以没有,如果需要指定 optValue,必须用等号 “=” 分隔,且不能有空格。另外还举了正例和反例。 - non-option arguments:没有 “–” 前缀的参数都将被视为 non-option arguments。
package org.springframework.core.env;
/**
* Parses a {@code String[]} of command line arguments in order to populate a
* {@link CommandLineArgs} object.
*
* <h3>Working with option arguments</h3>
* <p>Option arguments must adhere to the exact syntax:
*
* <pre class="code">--optName[=optValue]</pre>
*
* <p>That is, options must be prefixed with "{@code --}" and may or may not
* specify a value. If a value is specified, the name and value must be separated
* <em>without spaces</em> by an equals sign ("="). The value may optionally be
* an empty string.
*
* <h4>Valid examples of option arguments</h4>
* <pre class="code">
* --foo
* --foo=
* --foo=""
* --foo=bar
* --foo="bar then baz"
* --foo=bar,baz,biz</pre>
*
* <h4>Invalid examples of option arguments</h4>
* <pre class="code">
* -foo
* --foo bar
* --foo = bar
* --foo=bar --foo=baz --foo=biz</pre>
*
* <h3>Working with non-option arguments</h3>
* <p>Any and all arguments specified at the command line without the "{@code --}"
* option prefix will be considered as "non-option arguments" and made available
* through the {@link CommandLineArgs#getNonOptionArgs()} method.
*
* @author Chris Beams
* @author Sam Brannen
* @since 3.1
*/
class SimpleCommandLineArgsParser {
/**
* Parse the given {@code String} array based on the rules described {@linkplain
* SimpleCommandLineArgsParser above}, returning a fully-populated
* {@link CommandLineArgs} object.
* @param args command line arguments, typically from a {@code main()} method
*/
public CommandLineArgs parse(String... args) {
CommandLineArgs commandLineArgs = new CommandLineArgs();
for (String arg : args) {
// 如果以 -- 开头
if (arg.startsWith("--")) {
// 截取 -- 之后的内容 optionText
String optionText = arg.substring(2);
String optionName;
String optionValue = null;
int indexOfEqualsSign = optionText.indexOf('=');
// optionText 中有等号,按照等号分隔 name 和 value
if (indexOfEqualsSign > -1) {
optionName = optionText.substring(0, indexOfEqualsSign);
optionValue = optionText.substring(indexOfEqualsSign + 1);
}
// optionText 中没有等号
else {
optionName = optionText;
}
// --、--=xxx 这种的会直接报错
if (optionName.isEmpty()) {
throw new IllegalArgumentException("Invalid argument syntax: " + arg);
}
// 将键值对放入 OptionArg
commandLineArgs.addOptionArg(optionName, optionValue);
}
// 将不以 -- 开头的放入 NonOptionArg
else {
commandLineArgs.addNonOptionArg(arg);
}
}
return commandLineArgs;
}
}
CommandLineArgs
类。
/**
* A simple representation of command line arguments, broken into "option arguments" and
* "non-option arguments".
*
* @author Chris Beams
* @since 3.1
* @see SimpleCommandLineArgsParser
*/
class CommandLineArgs {
// 存放可解析的键值对的参数,同名的会被放在 List 中
private final Map<String, List<String>> optionArgs = new HashMap<>();
// 存放非键值对参数
private final List<String> nonOptionArgs = new ArrayList<>();
/**
* Add an option argument for the given option name and add the given value to the
* list of values associated with this option (of which there may be zero or more).
* The given value may be {@code null}, indicating that the option was specified
* without an associated value (e.g. "--foo" vs. "--foo=bar").
*/
public void addOptionArg(String optionName, @Nullable String optionValue) {
// 没有键为 optionName 的要先初始化 List
if (!this.optionArgs.containsKey(optionName)) {
this.optionArgs.put(optionName, new ArrayList<>());
}
// optionValue 不为空才放入
if (optionValue != null) {
this.optionArgs.get(optionName).add(optionValue);
}
}
/**
* Return the set of all option arguments present on the command line.
*/
public Set<String> getOptionNames() {
return Collections.unmodifiableSet(this.optionArgs.keySet());
}
/**
* Return whether the option with the given name was present on the command line.
*/
public boolean containsOption(String optionName) {
return this.optionArgs.containsKey(optionName);
}
/**
* Return the list of values associated with the given option. {@code null} signifies
* that the option was not present; empty list signifies that no values were associated
* with this option.
*/
@Nullable
public List<String> getOptionValues(String optionName) {
return this.optionArgs.get(optionName);
}
/**
* Add the given value to the list of non-option arguments.
*/
public void addNonOptionArg(String value) {
this.nonOptionArgs.add(value);
}
/**
* Return the list of non-option arguments specified on the command line.
*/
public List<String> getNonOptionArgs() {
return Collections.unmodifiableList(this.nonOptionArgs);
}
}