使用过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的相关源码解析完成。