目录
一、背景
上一篇我们解读了监听器的解析,本篇主要解读命令行参数解析,首先我们还是先回顾下启动的整体流程。
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 source,DefaultApplicationArguments就可以通过自身的方法获取命令行数的参数了。
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
结语
通过本文我们了解了命令行参数的解析与封装,让大家不至于那么迷茫,下一篇我们开始解读环境准备的源码,就需要用到这个命令行参数(如果有)。