Alian解读SpringBoot 2.6.0 源码(三):启动流程分析之命令行参数解析

一、背景

  上一篇我们解读了监听器的解析,本篇主要解读命令行参数解析,首先我们还是先回顾下启动的整体流程。

1.1、run方法整体流程

  接下来的几个方法所在类的具体路径:org.springframework.boot.SpringApplication

	public ConfigurableApplicationContext run(String... args) {
		// 1、记录启动的开始时间(单位纳秒)
		long startTime = System.nanoTime();
		
		// 2、初始化启动上下文、初始化应用上下文
		DefaultBootstrapContext bootstrapContext = createBootstrapContext();
		ConfigurableApplicationContext context = null;

		// 3、设置无头属性:“java.awt.headless”,默认值为:true(没有图形化界面)
		configureHeadlessProperty();

		// 4、获取所有 Spring 运行监听器
		SpringApplicationRunListeners listeners = getRunListeners(args);
		// 发布应用启动事件
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
			// 5、初始化默认应用参数类(命令行参数)
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

			// 6、根据运行监听器和应用参数 来准备 Spring 环境
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
			// 配置忽略bean信息
			configureIgnoreBeanInfo(environment);

			// 7、创建 Banner 并打印
			Banner printedBanner = printBanner(environment);

			// 8、创建应用上下文
			context = createApplicationContext();
			// 设置applicationStartup
			context.setApplicationStartup(this.applicationStartup);

			// 9、准备应用上下文
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

			// 10、刷新应用上下文(核心)
			refreshContext(context);

			// 11、应用上下文刷新后置处理
			afterRefresh(context, applicationArguments);

			// 13、时间信息、输出日志记录执行主类名
			Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
			}

			// 14、发布应用上下文启动完成事件
			listeners.started(context, timeTakenToStartup);

			// 15、执行所有 Runner 运行器
			callRunners(context, applicationArguments);
		} catch (Throwable ex) {
			// 运行错误处理
			handleRunFailure(context, ex, listeners);
			throw new IllegalStateException(ex);
		}
		try {
			// 16、发布应用上下文就绪事件(可以使用了)
			Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
			listeners.ready(context, timeTakenToReady);
		} catch (Throwable ex) {
			// 运行错误处理
			handleRunFailure(context, ex, null);
			throw new IllegalStateException(ex);
		}
		// 17、返回应用上下文
		return context;
	}

1.2、本文解读范围

  args 是启动Spring应用的命令行参数,该参数可以在Spring应用中被访问。本文主要讲解到命令行参数的解析,也就是:

	// 5、初始化默认应用参数类(命令行参数)
	ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

二、默认应用参数解析

2.1、接口ApplicationArguments

  接口的具体路径:org.springframework.boot.ApplicationArguments

package org.springframework.boot;

import java.util.List;
import java.util.Set;

/**
 * 提供对用于运行的参数的访问
 */
public interface ApplicationArguments {

	/**
	 * 返回传递给应用程序的原始未处理参数
	 * 
	 * @return the arguments
	 */
	String[] getSourceArgs();

	/**
	 * 获取选项参数名称
	 * 
	 * 如果参数是“--foo=bar--debug”将返回包含“foo”和“debug”的Set集合。
	 * @return 选项名称集合或一个空集
	 */
	Set<String> getOptionNames();

	/**
	 * 返回从参数解析的选项参数集是否包含具有给定名称的选项
	 * 
	 * @param name 要检查的名称
	 * @return 如果参数包含具有给定名称的选项返回true
	 */
	boolean containsOption(String name);

	/**
	 * 根据名称获取可选参数
	 * 
	 * 如果选项存在且没有参数(例如:“--foo”),则返回一个空值集合
	 * 如果选项存在且只有一个值(例如“--foo=bar”),则返回一个具有一个元素的集合
	 * 如果该选项存在且具有多个值(例如“--foo=bar--foo=baz”),返回具有多个元素的一个集合
	 * 如果该选项不存在,则返回null
	 *
	 *@param name 选项的名称
	 *@return 给定名称的选项值列表
	 */
	List<String> getOptionValues(String name);

	/**
	 * 获取非选项参数
	 * 
	 * @return 非选项参数或空列表
	 */
	List<String> getNonOptionArgs();

}

ApplicationArguments是提供对用于运行的参数访问的一个类,可选参数和非选项参数

  • 可选参数:指的是带有 “–” 开头的参数,会被解析成一个map(–server.port=8080,key是server.port,value是8080)
  • 非选项参数:不是“–”开头的参数,会放到非选项的List中

2.2、实现类DefaultApplicationArguments

2.2.1 实现类源码

DefaultApplicationArguments.java

package org.springframework.boot;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.core.env.SimpleCommandLinePropertySource;
import org.springframework.util.Assert;

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");
		// 实例化Source (静态内部类)
		this.source = new Source(args);
		this.args = args;
	}

	@Override
	public String[] getSourceArgs() {
		return this.args;
	}

	@Override
	public Set<String> getOptionNames() {
		String[] names = this.source.getPropertyNames();
		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();
	}

	private static class Source extends SimpleCommandLinePropertySource {

		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 的一个静态内部类,是一个非常关键的内容。

2.2.2 Source类图

  上面也说到Source用得多,我们先看下Source类图,有助于我们开始接下里的流程分析。
在这里插入图片描述

2.3、流程分析

  还是run方法开始的调用开始。

2.3.1、初始化DefaultApplicationArguments
	// 5、初始化默认应用参数类
	ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

  调用到我们实现类的构造方法: DefaultApplicationArguments(String… args),在此构造方法中继续实例化静态内部类Source

	public DefaultApplicationArguments(String... args) {
		Assert.notNull(args, "Args must not be null");
		// 实例化静态内部类Source
		this.source = new Source(args);
		this.args = args;
	}

	// 静态内部类
	private static class Source extends SimpleCommandLinePropertySource {
		
		Source(String[] args) {
			// 调用父类的构造方法:SimpleCommandLinePropertySource(String[] args)
			super(args);
		}

	}

  在静态内部类Source中继续调用它父类的构造方法:SimpleCommandLinePropertySource(String[] args)

public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> {

	public SimpleCommandLinePropertySource(String... args) {
		// 通过SimpleCommandLineArgsParser进行解析
		super(new SimpleCommandLineArgsParser().parse(args));
	}
}
2.3.2、命令行参数解析

  我们具体看下SimpleCommandLineArgsParser 怎么解析的

package org.springframework.core.env;

class SimpleCommandLineArgsParser {

	public CommandLineArgs parse(String... args) {
		// 初始化命令行参数
		CommandLineArgs commandLineArgs = new CommandLineArgs();
		// 遍历传递的参数
		for (String arg : args) {
			// 如果是两个短横线开头("--")也就是可选参数
			if (arg.startsWith("--")) {
				// 去除两个短横线
				String optionText = arg.substring(2);
				// 定义参数名称
				String optionName;
				// 定义参数值,默认为null(当以"--"开头,但是不包含等号,有参数名时使用到)
				String optionValue = null;
				// 传递参数是否包含"="
				int indexOfEqualsSign = optionText.indexOf('=');
				if (indexOfEqualsSign > -1) {
					// 包"="则用等号进行分割,取到参数名称和参数值
					optionName = optionText.substring(0, indexOfEqualsSign);
					optionValue = optionText.substring(indexOfEqualsSign + 1);
				} else {
					// 不包含"="则直接当做参数名称
					optionName = optionText;
				}
				// 参数名称不能为空
				if (optionName.isEmpty()) {
					throw new IllegalArgumentException("Invalid argument syntax: " + arg);
				}
				// 可选参数:把参数名称和参数值封装
				commandLineArgs.addOptionArg(optionName, optionValue);
			} else {
				// 非选项参数直接加入
				commandLineArgs.addNonOptionArg(arg);
			}
		}
		// 返回结果
		return commandLineArgs;
	}
}

  实现的主要逻辑是:对传递过来的参数进行遍历,判断每一组参数是否是以两个短横线("–")开头,

  • 如果是两个短横线开头,去除开头的两个短横线,再判断是否含有等号,如果包含等号,就用等号进行分割,取到参数名称和参数值;如果不包含等号,则只有参数名称,参数值为空,最后把取得的参数名称和参数值,加入到可选参数列表Map
  • 如果不是两个短横线开头,则加入到非可选参数列表List

  上面用到的 CommandLineArgs 就是一个Map和List,如下:

class CommandLineArgs {
	// 可选参数
	private final Map<String, List<String>> optionArgs = new HashMap<>();
	// 非选项参数
	private final List<String> nonOptionArgs = new ArrayList<>();
}

2.4、参数封装

  通过上面参数的解析我们得到了一个封装好命令行参数的对象 CommandLineArgs
也就是

	CommandLineArgs commandLineArgs=new SimpleCommandLineArgsParser().parse(args)

还记得我们之前的方法么?通过实例化静态内部类Source
  通过一系列的父类的构造方法,最终PropertySource<T>的属性:protected final T source拥有了传入的值,此时的 T 也就是 CommandLineArgs ,具体构造调用如下:

public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> {

	public SimpleCommandLinePropertySource(String... args) {
		super(new SimpleCommandLineArgsParser().parse(args));
	}
}
public abstract class CommandLinePropertySource<T> extends EnumerablePropertySource<T> {
	
	public static final String COMMAND_LINE_PROPERTY_SOURCE_NAME = "commandLineArgs";
	
	public CommandLinePropertySource(T source) {
		super(COMMAND_LINE_PROPERTY_SOURCE_NAME, source);
	}
}
public abstract class EnumerablePropertySource<T> extends PropertySource<T> {
	
	public EnumerablePropertySource(String name, T source) {
		super(name, source);
	}
}
public abstract class PropertySource<T> {

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

  而Source相当于是PropertySource<T>的实现类,同时 Source 又是 DefaultApplicationArguments 的内部类,实例化Source赋值给 DefaultApplicationArguments 的属性private final Source sourceDefaultApplicationArguments就可以通过自身的方法获取命令行数的参数了。

2.5、实际演示

  假设我们系统启动要加入如下参数: –server.port=9000 --server.servlet.context-path=/spring-args Alian CSDN

2.5.1 命令行参数设置图

  使用开发工具 idea,命令行参数设置如下图
在这里插入图片描述

2.5.2 测试及结果

  我们直接在 main 方法里写个测试代码,通过对象 ApplicationArguments 来获取我们传入的参数。

package com.alian.springboot;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.Arrays;
import java.util.List;

@SpringBootApplication
public class SpringbootApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootApplication.class, args);
        System.out.println("---------------可选参数----------------");
        ApplicationArguments arguments = context.getBean(ApplicationArguments.class);
        System.out.println("所有可选参数的名称列表:" + arguments.getOptionNames());
        System.out.println("获取到的应用上下路径:" + arguments.getOptionValues("server.servlet.context-path").get(0));
        System.out.println("获取到的端口:" + arguments.getOptionValues("server.port").get(0));
        System.out.println("---------------非选项参数----------------");
        List<String> nonOptionArgs = arguments.getNonOptionArgs();
        for (String nonOptionArg : nonOptionArgs) {
            System.out.println("获取到的非选项参数:" + nonOptionArg);
        }
        System.out.println("---------------传入的源参数----------------");
        Arrays.stream(arguments.getSourceArgs()).forEach(System.out::println);
    }

}

  调试获取结果图:
在这里插入图片描述
控制台输出结果:

2021-12-01 15:14:44 694 [main] INFO logStarting 55:Starting SpringbootApplication using Java 1.8.0_111 on DESKTOP-EIGL04G with PID 15496 (C:\workspace\study\springboot\target\classes started by admin in C:\workspace\study\springboot)
2021-12-01 15:14:44 695 [main] INFO logStartupProfileInfo 659:No active profile set, falling back to default profiles: default
2021-12-01 15:14:45 244 [main] INFO initialize 108:Tomcat initialized with port(s): 9000 (http)
2021-12-01 15:14:45 249 [main] INFO log 173:Initializing ProtocolHandler ["http-nio-9000"]
2021-12-01 15:14:45 249 [main] INFO log 173:Starting service [Tomcat]
2021-12-01 15:14:45 250 [main] INFO log 173:Starting Servlet engine: [Apache Tomcat/9.0.48]
2021-12-01 15:14:45 293 [main] INFO log 173:Initializing Spring embedded WebApplicationContext
2021-12-01 15:14:45 293 [main] INFO prepareWebApplicationContext 290:Root WebApplicationContext: initialization completed in 569 ms
2021-12-01 15:14:45 477 [main] INFO log 173:Starting ProtocolHandler ["http-nio-9000"]
2021-12-01 15:14:45 487 [main] INFO start 220:Tomcat started on port(s): 9000 (http) with context path '/spring-args'
2021-12-01 15:14:45 502 [main] INFO logStarted 61:Started SpringbootApplication in 1.067 seconds (JVM running for 1.559)
---------------可选参数----------------
所有可选参数的名称列表:[server.servlet.context-path, server.port]
获取到的应用上下路径:/spring-args
获取到的端口:9000
---------------非选项参数----------------
获取到的非选项参数:Alian
获取到的非选项参数:CSDN
---------------传入的源参数----------------
--server.port=9000
--server.servlet.context-path=/spring-args
Alian
CSDN

结语

  通过本文我们了解了命令行参数的解析与封装,让大家不至于那么迷茫,下一篇我们开始解读环境准备的源码,就需要用到这个命令行参数(如果有)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值