美文网首页
Spring Boot启动命令参数详解与优先级问题

Spring Boot启动命令参数详解与优先级问题

作者: CoderMichael | 来源:发表于2022-03-22 13:36 被阅读0次

使用过Spring Boot,我们都知道通过java -jar可以快速启动Spring Boot项目。同时,也可以通过在执行jar -jar时传递参数来进行配置。本文带大家系统的了解一下Spring Boot命令行参数相关的功能及相关源码分析。

1. 命令行参数使用

启动Spring Boot项目时,我们可以通过如下方式传递参数:

java -jar xxx.jar --server.port=8081

默认情况下Spring Boot使用8080端口,通过上述参数将其修改为8081端口,而且通过命令行传递的参数具有更高的优先级,会覆盖同名的其他配置参数。

启动Spring Boot项目时传递参数,有三种参数形式:

  • 选项参数
  • 非选项参数
  • 系统参数

选项参数,上面的示例便是选项参数的使用方法,通过“–-server.port”来设置应用程序的端口。基本格式为“--name=value”。其配置作用等价于在application.properties中配置的server.port=8081。

非选项参数的使用示例如下:

java -jar xxx.jar abc def 

上述示例中,“abc” 和 “def” 便是非选项参数。

系统参数,该参数会被设置到系统变量中,使用示例如下:

java -jar -Dserver.port=8081 xxx.jar

2. 参数值的获取

选项参数和非选项参数均可以通过ApplicationArguments接口获取,具体获取方法直接在使用参数的类中注入该接口即可。

@RestController
public class ArgumentsController {
    @Resource
    private ApplicationArguments arguments;
}

通过ApplicationArguments接口提供的方法即可获得对应的参数。关于该接口后面会详细讲解。

另外,选项参数,也可以直接通过@Value在类中获取,如下:

@RestController
public class ParamController {
    @Value("${server.port}")
    private String serverPort;
}

系统参数可以通过java.lang.System提供的方法获取:

String systemServerPort = System.getProperty("server.port");

3. 参数值的区别

关于参数值区别,重点看选项参数和系统参数。通过上面的示例我们已经发现使用选项参数时,参数在命令中是位于xxx.jar之后传递的,而系统参数是紧随java -jar之后。

如果不按照该顺序进行执行,比如使用如下方式使用选项参数:

java -jar --server.port=8081 xxx.jar

则会抛出如下异常:

Unrecognized option: --server.port=8081
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

如果将系统参数放在jar包后面,问题会更严重。会出现可以正常启动,但参数无法生效。这也是为什么有时候明明传递了参数但是却未生效,那很可能是因为把参数的位置写错了。

这个错误是最坑的,所以一定谨记:通过-D传递系统参数时,务必放置在待执行的jar包之前。

另外一个重要的不同是:通过@Value形式可以获得系统参数和选项参数,但通过System.getProperty方法只能获得系统参数。

4. SpringBoot配置优先级

==SpringBoot常用外部化配置方法,其中优先级数值越高,优先级越大,会覆盖优先级底的配置。==

配置文件方式(优先级3)

boostrap.yml内指定spring.cloud.nacos.config.server-addr参数参考如下:

spring:
  application:
    name: service-a
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yml
      discovery:
        server-addr: 127.0.0.1:8848

项目打包

target/service-a-0.0.1-SNAPSHOT.jar

系统环境变量参数方式(优先级5)

export SPRING_CLOUD_NACOS_CONFIG_SERVERADDR=127.0.0.4:8848
java -jar target/service-a-0.0.1-SNAPSHOT.jar

注意:环境变量同意采用大写字母,不允许使用.-符号,采用下划线“_”取代点“.” 减号“-”直接删除。

说明:系统环境变量方式自由度高,可配合k8s部署脚本,动态切换到各运行环境

参考如下:

kind: Deployment
apiVersion: apps/v1
metadata:
  name: service-a
spec:
  template:
    spec:      
      containers:
        - name: service-a
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: prod
            - name: JAVA_OPTIONS
              value: '-XX:MaxRAMFraction=2'
            - name: SPRING_CLOUD_NACOS_CONFIG_SERVERADDR
              value: '10.0.0.2:8848'            
            - name: SPRING_CLOUD_NACOS_DISCOVERY_SERVERADDR
              value: '10.0.0.2:8848'

Java系统参数方式(优先级6)

java -jar target/service-a-0.0.1-SNAPSHOT.jar -Dspring.cloud.nacos.config.server-addr=127.0.0.2:8848

命令行参数方式(优先级11)

java -jar target/service-a-0.0.1-SNAPSHOT.jar --spring.cloud.nacos.config.server-addr=127.0.0.3:8848

具体的加载顺序、优先级参考如下文档描述:

Spring Boot uses a very particular PropertySource order that is designed to allow sensible
overriding of values. Properties are considered in the following order (with values from lower
items overriding earlier ones):

1. Default properties (specified by setting SpringApplication.setDefaultProperties).
2. @PropertySource annotations on your @Configuration classes. Please note that such property
   sources are not added to the Environment until the application context is being refreshed. This is
   too late to configure certain properties such as logging.* and spring.main.* which are read
   before refresh begins.
3. Config data (such as application.properties files)
4. A RandomValuePropertySource that has properties only in random.*.
5. OS environment variables.
6. Java System properties (System.getProperties()).
7. JNDI attributes from java:comp/env.
8. ServletContext init parameters.
9. ServletConfig init parameters.
10. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or
    system property).
11. Command line arguments.
12. properties attribute on your tests. Available on @SpringBootTest and the test annotations for
    54testing a particular slice of your application.
13. @TestPropertySource annotations on your tests.
14. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is
    active.

5. ApplicationArguments解析

上面提到了可以通过注入ApplicationArguments接口获得相关参数,下面看一下具体的使用示例:

@RestController
public class ArgumentsController {

    @Resource
    private ApplicationArguments arguments;

    @GetMapping("/args")
    public String getArgs() {

        System.out.println("# 非选项参数数量: "   arguments.getNonOptionArgs().size());
        System.out.println("# 选项参数数量: "   arguments.getOptionNames().size());
        System.out.println("# 非选项具体参数:");
        arguments.getNonOptionArgs().forEach(System.out::println);

        System.out.println("# 选项参数具体参数:");
        arguments.getOptionNames().forEach(optionName -> {
            System.out.println("--"   optionName   "="   arguments.getOptionValues(optionName));
        });

        return "success";
    }
}

通过注入ApplicationArguments接口,然后在方法中调用该接口的方法即可获得对应的参数信息。

ApplicationArguments接口中封装了启动时原始参数的数组、选项参数的列表、非选项参数的列表以及选项参数获得和检验。相关源码如下:

public interface ApplicationArguments {

    /**
     * 原始参数数组(未经过处理的参数)
     */
    String[] getSourceArgs();

    /**
     * 选项参数名称
     */
    Set<String> getOptionNames();

    /**
     * 根据名称校验是否包含选项参数
     */
    boolean containsOption(String name);

    /**
     * 根据名称获得选项参数
     */
    List<String> getOptionValues(String name);

    /**
     * 获取非选项参数列表
     */
    List<String> getNonOptionArgs();
}

6. 命令行参数的解析

上面直接使用了ApplicationArguments的注入和方法,那么它的对象是何时被创建,何时被注入Spring容器的?

在执行SpringApplication的run方法的过程中会获得传入的参数,并封装为ApplicationArguments对象。相关源代码如下:

public ConfigurableApplicationContext run(String... args) {
        
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // ...
        prepareContext(context, environment, listeners, // ...
    } catch (Throwable ex) {
        // ...
    }
    return context;
}

在上述代码中,通过创建一个它的实现类DefaultApplicationArguments来完成命令行参数的解析。

DefaultApplicationArguments部分代码如下:

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");
        this.source = new Source(args);
        this.args = args;
    }
    
    // ...

    @Override
    public List<String> getOptionValues(String name) {
        List<String> values = this.source.getOptionValues(name);
        return (values != null) ? Collections.unmodifiableList(values) : null;
    }

    private static class Source extends SimpleCommandLinePropertySource {
        Source(String[] args) {
            super(args);
        }
        // ...
    }
}

通过构造方法,将args赋值给成员变量args,其中接口ApplicationArguments中getSourceArgs方法的实现在该类中便是返回args值。

针对成员变量Source(内部类)的设置,在创建Source对象时调用了其父类SimpleCommandLinePropertySource的构造方法:

public SimpleCommandLinePropertySource(String... args) {
    super(new SimpleCommandLineArgsParser().parse(args));
}

在该方法中创建了真正的解析器SimpleCommandLineArgsParser并调用其parse方法对参数进行解析。

class SimpleCommandLineArgsParser {

    public CommandLineArgs parse(String... args) {
        CommandLineArgs commandLineArgs = new CommandLineArgs();
        for (String arg : args) {
            // --开头的选参数解析
            if (arg.startsWith("--")) {
                // 获得key=value或key值
                String optionText = arg.substring(2, arg.length());
                String optionName;
                String optionValue = null;
                // 如果是key=value格式则进行解析
                if (optionText.contains("=")) {
                    optionName = optionText.substring(0, optionText.indexOf('='));
                    optionValue = optionText.substring(optionText.indexOf('=') 1, optionText.length());
                } else {
                    // 如果是仅有key(--foo)则获取其值
                    optionName = optionText;
                }
                // 如果optionName为空或者optionValue不为空但optionName为空则抛出异常
                if (optionName.isEmpty() || (optionValue != null && optionValue.isEmpty())) {
                    throw new IllegalArgumentException("Invalid argument syntax: "   arg);
                }
                // 封装入CommandLineArgs
                commandLineArgs.addOptionArg(optionName, optionValue);
            } else {
                commandLineArgs.addNonOptionArg(arg);
            }
        }
        return commandLineArgs;
    }
}

上述解析规则比较简单,就是根据“–”和“=”来区分和解析不同的参数类型。

通过上面的方法创建了ApplicationArguments的实现类的对象,但此刻还并未注入Spring容器,注入Spring容器是依旧是通过上述SpringApplication#run方法中调用的prepareContext方法来完成的。相关代码如下:

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
        SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
    // ...
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    // 通过beanFactory将ApplicationArguments的对象注入Spring容器
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    // ...
}

至此关于Spring Boot中ApplicationArguments的相关源码解析完成。

参考:

  1. 原文链接:《Spring Boot启动命令参数详解及源码分析
  2. Spring Boot 外部化配置 参数 变量 优先级 覆盖配置参数

相关文章

网友评论

      本文标题:Spring Boot启动命令参数详解与优先级问题

      本文链接:https://www.haomeiwen.com/subject/tfbzdrtx.html