SpringBoot源码系列文章
SpringBoot源码解析(一):SpringApplication构造方法
SpringBoot源码解析(二):引导上下文DefaultBootstrapContext
SpringBoot源码解析(三):启动开始阶段
SpringBoot源码解析(四):解析应用参数args
目录
- 前言
- 一、入口
- 二、默认应用程序参数DefaultApplicationArguments
- 1、功能概述
- 2、使用示例
- 3、接口ApplicationArguments
- 4、实现类DefaultApplicationArguments
- 四、Source
- 1、属性源PropertySource
- 2、枚举属性源EnumerablePropertySource
- 3、命令行属性源CommandLinePropertySource
- 4、简单命令行属性源SimpleCommandLinePropertySource
- 五、解析参数原理
- 1、解析方法
- 2、解析参数的存储和访问
- 3、实际应用
- 总结
前言
前文深入解析了SpringBoot启动的开始阶段,包括获取和启动应用启动监听器、事件与广播机制,以及如何通过匹配监听器实现启动过程各阶段的自定义逻辑。接下来,我们将探讨SpringBoot启动类main函数中的参数args的作用及其解析过程
。
SpringBoot版本2.7.18
SpringApplication的run方法的执行逻辑如下,本文将详细介绍第3小节:解析应用参数
// SpringApplication类方法
public ConfigurableApplicationContext run(String... args) {
// 记录应用启动的开始时间
long startTime = System.nanoTime();
// 1.创建引导上下文,用于管理应用启动时的依赖和资源
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
// 配置无头模式属性,以支持在无图形环境下运行
// 将系统属性 java.awt.headless 设置为 true
configureHeadlessProperty();
// 2.获取Spring应用启动监听器,用于在应用启动的各个阶段执行自定义逻辑
SpringApplicationRunListeners listeners = getRunListeners(args);
// 启动开始方法(发布开始事件、通知应用监听器ApplicationListener)
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 3.解析应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 4.准备应用环境,包括读取配置文件和设置环境变量
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// 配置是否忽略 BeanInfo,以加快启动速度
configureIgnoreBeanInfo(environment);
// 5.打印启动Banner
Banner printedBanner = printBanner(environment);
// 6.创建应用程序上下文
context = createApplicationContext();
// 设置应用启动的上下文,用于监控和管理启动过程
context.setApplicationStartup(this.applicationStartup);
// 7.准备应用上下文,包括加载配置、添加 Bean 等
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 8.刷新上下文,完成 Bean 的加载和依赖注入
refreshContext(context);
// 9.刷新后的一些操作,如事件发布等
afterRefresh(context, applicationArguments);
// 计算启动应用程序的时间,并记录日志
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
// 10.通知监听器应用启动完成
listeners.started(context, timeTakenToStartup);
// 11.调用应用程序中的 `CommandLineRunner` 或 `ApplicationRunner`,以便执行自定义的启动逻辑
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
// 12.处理启动过程中发生的异常,并通知监听器
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
// 13.计算应用启动完成至准备就绪的时间,并通知监听器
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
}
catch (Throwable ex) {
// 处理准备就绪过程中发生的异常
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
// 返回已启动并准备就绪的应用上下文
return context;
}
一、入口
- 将main方法的参数args封装成一个对象
DefaultApplicationArguments
,以便方便地解析和访问启动参数
// 3.解析应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
二、默认应用程序参数DefaultApplicationArguments
1、功能概述
DefaultApplicationArguments
是SpringBoot中的一个类,用于处理启动时传入的参数。它实现了ApplicationArguments
接口,并提供了一些便捷的方法来访问传入的命令行参数
和选项参数
。
// 3.解析应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
- 解析命令行参数:将
main
方法中的args
参数解析成选项参数和非选项参数,方便应用在启动时读取外部传入的配置 - 访问
选项参数
:支持以--key=value
格式的选项参数,通过方法getOptionNames()
和getOptionValues(String name)
获取特定的选项及其值 - 访问
非选项参数
:对于不以--
开头的参数,可以通过getNonOptionArgs()
获取它们的列表
2、使用示例
- 假设我们在命令行中运行应用,传递了一些参数
java -jar myapp.jar --server.port=8080 arg1 arg2
- 在代码中,我们可以使用
DefaultApplicationArguments
来解析这些参数
public static void main(String[] args) {
DefaultApplicationArguments appArgs = new DefaultApplicationArguments(args);
// 获取所有选项参数名称
System.out.println("选项参数:" + appArgs.getOptionNames()); // 输出: ["server.port"]
// 获取指定选项的值(所有以 `--` 开头的选项参数名称)
System.out.println("server.port 值:" + appArgs.getOptionValues("server.port")); // 输出: ["8080"]
// 获取非选项参数(所有不以 `--` 开头的参数,通常用于传递无标记的参数值)
System.out.println("非选项参数:" + appArgs.getNonOptionArgs()); // 输出: ["arg1", "arg2"]
}
3、接口ApplicationArguments
- ApplicationArguments是
DefaultApplicationArguments
类的父接口
// 提供对用于运行应用的参数的访问。
public interface ApplicationArguments {
// 返回传递给应用程序的原始未处理参数
String[] getSourceArgs();
// 返回所有选项参数的名称
Set<String> getOptionNames();
// 返回解析的选项参数集合中是否包含具有给定名称的选项
boolean containsOption(String name);
// 返回与给定名称的选项参数关联的值集合
List<String> getOptionValues(String name);
// 返回解析的非选项参数集合
List<String> getNonOptionArgs();
}
getOptionNames()
:返回所有选项参数的名称- 例如:参数是
"--foo=bar --debug"
,则返回["foo", "debug"]
- 例如:参数是
getOptionValues(String name)
:返回与给定名称的选项参数关联的值集合- 如果选项存在但没有值(例如:
"--foo"
),返回一个空集合 - 如果选项存在且有单一值(例如:
"--foo=bar"
),返回一个包含一个元素的集合["bar"]
- 如果选项存在且有多个值(例如:
"--foo=bar --foo=baz"
),返回包含每个值的集合["bar", "baz"]
- 如果选项不存在,返回null
- 如果选项存在但没有值(例如:
getNonOptionArgs()
:返回解析的非选项参数集合
4、实现类DefaultApplicationArguments
- 代码很简单,对外暴露使用
DefaultApplicationArguments
,内部实现都在Source
中
// ApplicationArguments的默认实现类,用于解析应用程序启动时传入的参数。
public class DefaultApplicationArguments implements ApplicationArguments {
private final Source source; // 用于解析和存储参数的内部辅助类
private final String[] args; // 启动时传入的原始参数
// 构造函数,使用传入的参数数组初始化对象
public DefaultApplicationArguments(String... args) {
Assert.notNull(args, "Args must not be null"); // 确保传入参数不为 null
this.source = new Source(args); // 使用内部类 Source 解析参数
this.args = args; // 保存原始参数
}
// 获取原始未处理的参数数组
@Override
public String[] getSourceArgs() {
return this.args;
}
// 获取所有选项参数的名称集合
@Override
public Set<String> getOptionNames() {
String[] names = this.source.getPropertyNames();
// 该集合不能被修改(即添加、删除元素等操作会抛出 UnsupportedOperationException 异常)
return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(names)));
}
// 检查是否包含指定名称的选项参数
@Override
public boolean containsOption(String name) {
return this.source.containsProperty(name);
}
// 获取指定名称的选项参数的值集合
@Override
public List<String> getOptionValues(String name) {
List<String> values = this.source.getOptionValues(name);
return (values != null) ? Collections.unmodifiableList(values) : null;
}
// 获取所有非选项参数(不以 "--" 开头的参数)
@Override
public List<String> getNonOptionArgs() {
return this.source.getNonOptionArgs();
}
// 内部类,用于处理和解析命令行参数
// 继承自 SimpleCommandLinePropertySource,可以获取选项参数和非选项参数
private static class Source extends SimpleCommandLinePropertySource {
// 使用参数数组初始化 Source
Source(String[] args) {
super(args);
}
// 获取所有非选项参数。
@Override
public List<String> getNonOptionArgs() {
return super.getNonOptionArgs();
}
// 获取指定名称的选项参数的值列表
@Override
public List<String> getOptionValues(String name) {
return super.getOptionValues(name);
}
}
}
四、Source
Source是DefaultApplicationArguments解析参数内部的真正实现类,类图如下,逐一分析。
1、属性源PropertySource
PropertySource
是Spring框架中的一个核心抽象类,用于表示属性(键值对)的来源
。通过将各种配置来源(如系统属性
、环境变量
、配置文件
等)封装为PropertySource对象,Spring可以提供统一的接口来读取和管理这些配置数据。
属性源名称
:每个PropertySource实例都具有唯一的名称
,用于区分不同的属性源属性源对象
:PropertySource<T>
是一个泛型类,其中T
代表具体的属性源类型- getProperty(String name):用于在属性源对象中检索具体的属性,name表示具体属性的键,返回具体属性的值
public abstract class PropertySource<T> {
protected final Log logger = LogFactory.getLog(getClass());
protected final String name; // 属性源的名称
protected final T source; // 属性源的数据源对象
// 使用给定的名称和源对象创建属性源
public PropertySource(String name, T source) {
Assert.hasText(name, "Property source name must contain at least one character");
Assert.notNull(source, "Property source must not be null");
this.name = name;
this.source = source;
}
// 使用给定的名称和一个新的Object对象作为底层源创建属性源
public PropertySource(String name) {
this(name, (T) new Object());
}
// 返回属性源名称
public String getName() {
return this.name;
}
// 返回属性源的底层源对象。
public T getSource() {
return this.source;
}
// 判断属性源是否包含给定名称的属性(子类可以实现更高效的算法)
// containsProperty和getProperty参数的name与上面定义的属性源名称的name不是一回事
public boolean containsProperty(String name) {
return (getProperty(name) != null);
}
// 返回与给定名称关联的属性值,如果找不到则返回null,子类实现
@Nullable
public abstract Object getProperty(String name);
...
}
2、枚举属性源EnumerablePropertySource
EnumerablePropertySource
继承自PropertySource,主要用于定义getPropertyNames()
方法,可以获取属性源对象中所有属性键的名称
。
public abstract class EnumerablePropertySource<T> extends PropertySource<T> {
// 使用给定的名称和源对象创建属性源(调用父类PropertySource的构造方法)
public EnumerablePropertySource(String name, T source) {
super(name, source);
}
// 也是调用父类构造
protected EnumerablePropertySource(String name) {
super(name);
}
// 判断属性源是否包含具有给定名称的属性(重新了PropertySource的此方法)
@Override
public boolean containsProperty(String name) {
return ObjectUtils.containsElement(getPropertyNames(), name);
}
// 返回所有属性的名称
public abstract String[] getPropertyNames();
}
3、命令行属性源CommandLinePropertySource
CommandLinePropertySource
是Spring框架中用于处理命令行参数
的PropertySource
实现。它可以将应用程序启动时传入的命令行参数解析成键值对
,便于在应用配置中使用。
- 命令行属性源名称默认为
commandLineArgs
getOptionValues(String name)
:通过命令行属性源(即选项参数键值对
)的键获取对应的值getNonOptionArgs()
:通过命令行属性源(即键默认为nonOptionArgs的非选项参数键值对
)获取对于的值- 例:java -jar your-app.jar --server.port=8081 --spring.profiles.active=prod arg1 arg2
- 选项参数会有
多个键值对
,key1为server.port,key2为spring.profiles.active - 非选项参数
永远只有一个键值对
,所有key都是nonOptionArgs
- 选项参数会有
public abstract class CommandLinePropertySource<T> extends EnumerablePropertySource<T> {
// CommandLinePropertySource实例的默认名称
public static final String COMMAND_LINE_PROPERTY_SOURCE_NAME = "commandLineArgs";
// 表示非选项参数的属性键的默认名称
public static final String DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME = "nonOptionArgs";
private String nonOptionArgsPropertyName = DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME;
// 创建一个新的命令行属性源,使用默认名称
public CommandLinePropertySource(T source) {
super(COMMAND_LINE_PROPERTY_SOURCE_NAME, source);
}
// 创建一个新的命令行属性源,具有给定名称
public CommandLinePropertySource(String name, T source) {
super(name, source);
}
// 可以通过set方法设置非选项参数的键的名称
public void setNonOptionArgsPropertyName(String nonOptionArgsPropertyName) {
this.nonOptionArgsPropertyName = nonOptionArgsPropertyName;
}
// 首先检查指定的名称是否是特殊的“非选项参数”属性,
// 如果是,则委托给抽象方法#getNonOptionArgs()
// 否则,委托并返回抽象方法#containsOption(String)
@Override
public final boolean containsProperty(String name) {
if (this.nonOptionArgsPropertyName.equals(name)) {
return !getNonOptionArgs().isEmpty();
}
return this.containsOption(name);
}
// 首先检查指定的名称是否是特殊的“非选项参数”属性,
// 如果是,则委托给抽象方法#getNonOptionArgs(),返回用逗号隔开字符串
// 否则,委托并返回抽象方法#getOptionValues(name),返回用逗号隔开字符串
@Override
@Nullable
public final String getProperty(String name) {
if (this.nonOptionArgsPropertyName.equals(name)) {
Collection<String> nonOptionArguments = getNonOptionArgs();
if (nonOptionArguments.isEmpty()) {
return null;
}
else {
return StringUtils.collectionToCommaDelimitedString(nonOptionArguments);
}
}
Collection<String> optionValues = getOptionValues(name);
if (optionValues == null) {
return null;
}
else {
return StringUtils.collectionToCommaDelimitedString(optionValues);
}
}
// 返回从命令行解析的选项参数集合中是否包含具有给定名称的选项
protected abstract boolean containsOption(String name);
// 返回与给定名称的选项参数关联的值集合
@Nullable
protected abstract List<String> getOptionValues(String name);
// 返回从命令行解析的非选项参数集合,永不为null
protected abstract List<String> getNonOptionArgs();
}
4、简单命令行属性源SimpleCommandLinePropertySource
SimpleCommandLinePropertySource
是Spring框架中的一个类,继承自CommandLinePropertySource,用于解析和处理命令行参数
。它设计为简单易用,通过接收一个字符串数组(即命令行参数 args),将参数分为"选项参数"
和"非选项参数"
两类。
- 命令行属性源对象类型为
CommandLineArgs
,通过new SimpleCommandLineArgsParser().parse(args)
获取
public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> {
// 构造函数:创建一个使用默认名称commandLineArgs的SimpleCommandLinePropertySource实例的命令行属性源
public SimpleCommandLinePropertySource(String... args) {
super(new SimpleCommandLineArgsParser().parse(args));
}
// 创建指定名称命令行属性源
public SimpleCommandLinePropertySource(String name, String[] args) {
super(name, new SimpleCommandLineArgsParser().parse(args));
}
// 获取所有选项参数的名称
@Override
public String[] getPropertyNames() {
return StringUtils.toStringArray(this.source.getOptionNames());
}
// 检查是否包含指定名称的选项
@Override
protected boolean containsOption(String name) {
return this.source.containsOption(name);
}
// 获取指定选项名称的值列表
@Override
@Nullable
protected List<String> getOptionValues(String name) {
return this.source.getOptionValues(name);
}
// 获取所有非选项参数的列表
@Override
protected List<String> getNonOptionArgs() {
return this.source.getNonOptionArgs();
}
}
五、解析参数原理
在上一节中,我们了解了应用程序参数args
被解析后的结构
和存储方式
。接下来,我们回到文章开头,详细解析参数是如何被逐步解析出来的。
// 3.解析应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
- 根据new DefaultApplicationArguments(args)寻找解析arg的位置
- 解析完arg调用父类CommandLinePropertySource的构造方法
1、解析方法
SimpleCommandLineArgsParser通过遍历传入的命令行参数数组,根据参数的格式,将参数解析并分为选项参数和非选项参数。
- 选项参数解析规则
- 选项参数
必须以--前缀开头
,例如 --name=John 或 --debug - 如果包含等号 =,= 左边的部分是选项名称,右边的部分是选项值
- 如果没有等号,则视为不带值的选项
- 如果获取不到选项名称(例如传入 --=value或–),抛出异常,表示参数格式无效
- 选项参数
- 非选项参数解析规则
- 所有
不以--开头
的参数被视为非选项参数
- 所有
class SimpleCommandLineArgsParser {
public CommandLineArgs parse(String... args) {
// 创建 CommandLineArgs 实例,用于存储解析结果
CommandLineArgs commandLineArgs = new CommandLineArgs();
for (String arg : args) { // 遍历每个命令行参数
if (arg.startsWith("--")) { // 如果参数以 "--" 开头,则视为选项参数
String optionText = arg.substring(2); // 去掉 "--" 前缀
String optionName; // 选项名称
String optionValue = null; // 选项值,默认为 null
int indexOfEqualsSign = optionText.indexOf('='); // 查找等号的位置
if (indexOfEqualsSign > -1) { // 如果找到了等号
optionName = optionText.substring(0, indexOfEqualsSign); // 等号前的部分为选项名称
optionValue = optionText.substring(indexOfEqualsSign + 1); // 等号后的部分为选项值
}
else {
optionName = optionText; // 如果没有等号,整个文本为选项名称,值为 null
}
// 如果选项名称为空,抛出异常,例如,只输入了 "--=" 或 "--"
if (optionName.isEmpty()) {
throw new IllegalArgumentException("Invalid argument syntax: " + arg);
}
// 将解析出的选项名称和值添加到 CommandLineArgs 对象中
commandLineArgs.addOptionArg(optionName, optionValue);
}
else {
// 如果参数不是选项参数,直接作为非选项参数添加到 CommandLineArgs 对象中
commandLineArgs.addNonOptionArg(arg);
}
}
return commandLineArgs; // 返回解析结果
}
}
- 属性源对象类型
CommandLineArgs
// 命令行参数的简单表示形式,分为“带选项参数”和“无选项参数”。
class CommandLineArgs {
// 存储带选项的参数,每个选项可以有一个或多个值
private final Map<String, List<String>> optionArgs = new HashMap<>();
// 存储无选项的参数
private final List<String> nonOptionArgs = new ArrayList<>();
// 为指定的选项名称添加一个选项参数,并将给定的值添加到与此选项关联的值列表中(可能有零个或多个)
public void addOptionArg(String optionName, @Nullable String optionValue) {
if (!this.optionArgs.containsKey(optionName)) {
this.optionArgs.put(optionName, new ArrayList<>());
}
if (optionValue != null) {
this.optionArgs.get(optionName).add(optionValue);
}
}
// 返回命令行中所有带选项的参数名称集合
public Set<String> getOptionNames() {
return Collections.unmodifiableSet(this.optionArgs.keySet());
}
// 判断命令行中是否包含指定名称的选项。
public boolean containsOption(String optionName) {
return this.optionArgs.containsKey(optionName);
}
// 返回与给定选项关联的值列表。
// 表示null表示该选项不存在;空列表表示该选项没有关联值。
@Nullable
public List<String> getOptionValues(String optionName) {
return this.optionArgs.get(optionName);
}
// 将给定的值添加到无选项参数列表中
public void addNonOptionArg(String value) {
this.nonOptionArgs.add(value);
}
// 返回命令行中指定的无选项参数列表
public List<String> getNonOptionArgs() {
return Collections.unmodifiableList(this.nonOptionArgs);
}
}
2、解析参数的存储和访问
解析方法很简单,所有内容都在SimpleCommandLineArgsParser的parse方法中完成。相比之下,存储
和访问
方式更为复杂。
存储位置位于属性源对象PropertySource
中。从代码可知,args表示命令行参数,因此属性源名称
为命令行属性源默认名称commandLineArgs
,属性源对象
为解析args后的键值对
。
访问查询方式的底层实现就是操作CommandLineArgs中的optionArgs(选项参数)
和nonOptionArgs(非选项参数)
两个集合,但此过程经过多次跳转,最终依次通过 DefaultApplicationArguments -> DefaultApplicationArguments#Source -> SimpleCommandLinePropertySource -> CommandLineArgs
获取,其中CommandLineArgs就是是命令行属性源对象。这种设计主要是为了提供更灵活、安全的访问方式,避免直接暴露内部数据结构带来的潜在风险。
3、实际应用
之前在SpringBoot基础(二):配置文件详解文章中有介绍过配置文件设置临时属性,这次回过头再来看,就很清晰明了了。
总结
- 在SpringBoot启动时,启动类main函数中的
args
参数被解析为两类选项参数
(如 --server.port=8080)非选项参数
(如 arg1、arg2)
- 对外暴露应用参数对象
ApplicationArguments
提供查询方法getOptionValues(String name)
方法可以获取选项参数getNonOptionArgs()
方法则用于获取非选项参数- 这些参数在启动过程的后续阶段可供使用