摘要:本文我们重点分析一下Springboot框架中的命令行参数的使用以及框架内部处理的命令行参数的原理。
众所周知,springboot项目可以有两种方式启动,第一种使用jar包;第二种使用war包。在使用jar方式的时候,我们可以在启动jar包的时候设置一些命令参数。
1.1 命令行参数使用
首先我们看一下如何使用在项目启动的时候设置命令行参数以及值。我这里使用的开发工具是Spring Tool Suite 版本是: 3.9.0.RELEASE。我们先建立一个工程文件,目录结构如下图所示:
Application内容如下:
1
@EnableConfigurationProperties
2 public class Application {
3 public static void main(String[] args) {
4 ConfigurableApplicationContext configurableApplicationContext = SpringApplication.run(Application.class, args);
5 }
6 }
怎么启动上述的类呢?操作步骤如下:
第一步:点击
第二步:
首先,我们需要点击①箭头处的新增按钮,然后就会建立一个Spring Boot App、其次我们输入命令行的参数,然后点击App即可完成设置并启动项目。上图中我们设置了三个变量,如下所示:
--foo=bar -foo=bar1 --foo=bar2
上述这三个变量我们该如何获取呢?这也是校验参数是否设置成功的一个途径吧相信大家都想知道下一步的操作,下面的实例代码还是在上文的两个步骤为前提下进行。实例代码如下:
1 @EnableConfigurationProperties
2 public class Application {
3 public static void main(String[] args) {
4 ConfigurableApplicationContext configurableApplicationContext = SpringApplication.run(Application.class, args);
5 String property = configurableApplicationContext.getEnvironment().getProperty("foo");
6 System.out.println(property);
7 }
8 }
运行上述的代码,控制台的输出信息如下:
bar,bar2
我们通过ConfigurableApplicationContext实例获取环境ConfigurableEnvironment实例对象,然后通过ConfigurableEnvironment获取属性foo。ConfigurableEnvironment大家暂时先有个印象,我们通过这个实例对象获取到项目中所有的配置属性信息。这个我们后续也会详细的进行讲解。看到上面的输出,发现输出的是bar,bar2
,然而bar1并没有输出?为什么呢?大家看下这三个命令行参数有何迥异,很显然bar,bar2的参数属性都是--开头的,而bar1是-开头的。那我们就很好奇springboot是如何获取这些参数值以及解析的呢?
上述中的启动类是Application,该类中的main方法去启动Springboot项目,既然是main方法,所以我们上文提到的三个命令行参数最终会被设置到args参数中的。这一点大家一定要注意了。SpringApplication.run方法会将args参数继续传递到SpringApplication类中让框架处理的。接下来,我们看一下SpringApplication.run方法中关于命令行参数的相关处理逻辑吧。
1.2 命令行参数原理
我们开始跟进SpringApplication类中的run(String... args)方法,相关的代码如下所示:
1 public ConfigurableApplicationContext run(String... args) {
2 ...//省略
3 ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
4 ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);
5 ...//省略
6 }
1.2.1 DefaultApplicationArguments类
首先,我们看一下DefaultApplicationArguments类,该类的核心代码如下:
1 public class DefaultApplicationArguments implements ApplicationArguments {
2 private final Source source;
3 private final String[] args;
4 public DefaultApplicationArguments(String[] args) {
5 Assert.notNull(args, "Args must not be null");
6 this.source = new Source(args);
7 this.args = args;
8 }
DefaultApplicationArguments类的构造函数中,首先实例化Source类,并将args参数以及值进行传递。然后在自身类中使用args属性进行参数值的报错,DefaultApplicationArguments类实现了ApplicationArguments接口。ApplicationArguments 接口中定义了各种命令行参数的操作,比如参数值的获取、参数名称的获取等方法。
上面的代码感觉没有什么神奇的地方,貌似只是对命令行参数的各种封装而已。其实Source类大有玄机。该类的代码如下所示:
1
private static class Source extends SimpleCommandLinePropertySource {
2 Source(String[] args) {
3 super(args);
4 }
5 ...//省略
6 }
Source类继承SimpleCommandLinePropertySource类,并在当前类的构造函数调用SimpleCommandLinePropertySource 类的构造函数,SimpleCommandLinePropertySource 类的构造函数如下所示:
1 public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> {
2 public SimpleCommandLinePropertySource(String... args) {
3 super(new SimpleCommandLineArgsParser().parse(args));
4 }
虽然SimpleCommandLinePropertySource类的构造函数继续调用CommandLinePropertySource类的构造函数进行处理,但是我们只需要将new SimpleCommandLineArgsParser().parse(args)这行代码搞明白,关于命令行参数以及值的处理我们就可以搞明白了。接下来我们快速看一下parse方法的实现逻辑,实例代码如下:
1 public CommandLineArgs parse(String... args) {
2 CommandLineArgs commandLineArgs = new CommandLineArgs();
3 for (String arg : args) {
4 if (arg.startsWith("--")) {
5 String optionText = arg.substring(2, arg.length());
6 String optionName;
7 String optionValue = null;
8 if (optionText.contains("=")) {
9 optionName = optionText.substring(0, optionText.indexOf("="));
10 optionValue = optionText.substring(optionText.indexOf("=")+1, optionText.length());
11 }
12 else {
13 optionName = optionText;
14 }
15 if (optionName.isEmpty() || (optionValue != null && optionValue.isEmpty())) {
16 throw new IllegalArgumentException("Invalid argument syntax: " + arg);
17 }
18 commandLineArgs.addOptionArg(optionName, optionValue);
19 }
20 else {
21 commandLineArgs.addNonOptionArg(arg);
22 }
23 }
24 return commandLineArgs;
25 }
我们将上述代码的逻辑梳理如下:
① 实例化CommandLineArgs类。这个类封装了命令行解析之后的参数以及值信息、还有没有被识别的命令行参数以及值的信息。
② 循环遍历所有的参数以及值args,比如我们传递的是--foo=bar -foo=bar1 --foo=bar2
③ 如果参数以--开头,则开始如下的操作;否则将其作为不能识别的参数进行处理,上文我们定义的-foo就是不能被识别的参数。
截取--字符串并使用optionText变量进行存储,比如参数--foo=bar截取之后,则optionText值为foo=bar。
optionName为foo,optionValue为bar,通过上述代码逻辑可以看出,如果=前边或者后边出现了空格就惨了。因为这个地方的代码并没有对空格以及特殊字符进行区分。
1.2.1.1. 识别参数添加
commandLineArgs.addOptionArg(optionName, optionValue);进行springboot可识别的命令行参数的添加工作,其内部实现逻辑如下所示:
1 private final Map<String, List<String>> optionArgs = new HashMap<>();
2 public void addOptionArg(String optionName, @Nullable String optionValue) {
3 if (!this.optionArgs.containsKey(optionName)) {
4 this.optionArgs.put(optionName, new ArrayList<>());
5 }
6 if (optionValue != null) {
7 this.optionArgs.get(optionName).add(optionValue);
8 }
9 }
上述代码中,首先校验optionName参数值是否存在于optionArgs集合中。如果不存在,则直接实例化ArrayList并将其添加到optionArgs集合中。
注意:optionArgs的key为参数的名称,value是一个List集合,存储的是该参数的所有值。通过这里的处理我们可以看出,我们上文输出的foo参数的值是bar,bar2就很容易理解了。
1.2.1.2. 未知参数添加
commandLineArgs.addNonOptionArg(arg)方法进行未知参数的添加逻辑,比如上文中的-foo=bar1参数就在这个方法进行处理的额,因为该参数不是--开头的。commandLineArgs.addNonOptionArg(arg)方法如下所示:
1 private final List<String> nonOptionArgs = new ArrayList<>();
2 public void addNonOptionArg(String value) {
3 this.nonOptionArgs.add(value);
4 }
未知参数最终被添加到了nonOptionArgs集合。
讲解到这里基本上命令行参数的设置以及解析原理都搞明白了。