ApplicationRunner、CommandLineRunner 的区别(源码)

ApplicationRunner、CommandLineRunner 的区别(源码)

ApplicationRunnerCommandLineRunner都能在 springboot 启动时执行一些初始化的工作。javadoc 中说明了如果需要访问ApplicationArguments而不是原始的String[]需要使用ApplicationRunner。两者run()方法的参数不同,分别为ApplicationArgumentsString... 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);
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值