SpringBoot源码解析(四):解析应用参数args

SpringBoot源码系列文章

SpringBoot源码解析(一):SpringApplication构造方法

SpringBoot源码解析(二):引导上下文DefaultBootstrapContext

SpringBoot源码解析(三):启动开始阶段

SpringBoot源码解析(四):解析应用参数args


目录

前言

前文深入解析了SpringBoot启动的开始阶段,包括获取和启动应用启动监听器、事件与广播机制,以及如何通过匹配监听器实现启动过程各阶段的自定义逻辑。接下来,我们将探讨SpringBoot启动类main函数中的参数args的作用及其解析过程

SpringBoot版本2.7.18SpringApplication的run方法的执行逻辑如下,本文将详细介绍第3小节:解析应用参数

java 复制代码
// 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,以便方便地解析和访问启动参数
java 复制代码
// 3.解析应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

二、默认应用程序参数DefaultApplicationArguments

1、功能概述

DefaultApplicationArguments是SpringBoot中的一个类,用于处理启动时传入的参数。它实现了ApplicationArguments接口,并提供了一些便捷的方法来访问传入的命令行参数选项参数

java 复制代码
// 3.解析应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
  • 解析命令行参数:将 main 方法中的 args 参数解析成选项参数和非选项参数,方便应用在启动时读取外部传入的配置
  • 访问选项参数:支持以--key=value格式的选项参数,通过方法getOptionNames()getOptionValues(String name)获取特定的选项及其值
  • 访问非选项参数:对于不以--开头的参数,可以通过getNonOptionArgs()获取它们的列表

2、使用示例

  • 假设我们在命令行中运行应用,传递了一些参数
sh 复制代码
java -jar myapp.jar --server.port=8080 arg1 arg2
  • 在代码中,我们可以使用DefaultApplicationArguments来解析这些参数
java 复制代码
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类的父接口
java 复制代码
// 提供对用于运行应用的参数的访问。
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
java 复制代码
// 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表示具体属性的键,返回具体属性的值
java 复制代码
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()方法,可以获取属性源对象中所有属性键的名称。**

java 复制代码
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
java 复制代码
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)获取
java 复制代码
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被解析后的结构存储方式。接下来,我们回到文章开头,详细解析参数是如何被逐步解析出来的。

java 复制代码
// 3.解析应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
  • 根据new DefaultApplicationArguments(args)寻找解析arg的位置
  • 解析完arg调用父类CommandLinePropertySource的构造方法

1、解析方法

SimpleCommandLineArgsParser通过遍历传入的命令行参数数组,根据参数的格式,将参数解析并分为选项参数和非选项参数。

  • 选项参数解析规则
    • 选项参数必须以--前缀开头,例如 --name=John 或 --debug
    • 如果包含等号 =,= 左边的部分是选项名称,右边的部分是选项值
    • 如果没有等号,则视为不带值的选项
    • 如果获取不到选项名称(例如传入 --=value或--),抛出异常,表示参数格式无效
  • 非选项参数解析规则
    • 所有不以--开头的参数被视为非选项参数
java 复制代码
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
java 复制代码
// 命令行参数的简单表示形式,分为"带选项参数"和"无选项参数"。
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() 方法则用于获取非选项参数
    • 这些参数在启动过程的后续阶段可供使用
相关推荐
鼠鼠我捏,要死了捏1 小时前
深入解析Java NIO多路复用原理与性能优化实践指南
java·性能优化·nio
ningqw1 小时前
SpringBoot 常用跨域处理方案
java·后端·springboot
你的人类朋友1 小时前
vi编辑器命令常用操作整理(持续更新)
后端
superlls1 小时前
(Redis)主从哨兵模式与集群模式
java·开发语言·redis
胡gh2 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
一只叫煤球的猫3 小时前
看到同事设计的表结构我人麻了!聊聊怎么更好去设计数据库表
后端·mysql·面试
uzong3 小时前
技术人如何对客做好沟通(上篇)
后端
叫我阿柒啊3 小时前
Java全栈工程师面试实战:从基础到微服务的深度解析
java·redis·微服务·node.js·vue3·全栈开发·电商平台
颜如玉3 小时前
Redis scan高位进位加法机制浅析
redis·后端·开源
Moment4 小时前
毕业一年了,分享一下我的四个开源项目!😊😊😊
前端·后端·开源