当执行“Java -jar springbootapp.jar --server.port=8081” 命令后会发生什么

本篇文章的起因是同事问我下面的Jar的启动命令中,- 和 –的参数有什么区别。下意识觉得区别是 在 xxx.jar 之前的参数叫做 VM 参数 ,传入JVM中,而后面的参数 Program 参数,是传入jar中的,对应的就是main(String[] args)中的args数组,但是说到有什么别的明显区别我倒是说不上来。

1
java -Xloggc:/logs/governor-service-gc.log -verbose.gc -XX:+PrintGCDateStamps -javaagent:/agent/apm-javaagent.jar -Dskywalking.collector.backend_service=11.11.11.11:1111 -Dskywalking.agent.service_name=uat-mgp-governor-xxxx -Dskywalking.agent.authentication=c8fxxxx17c46bd -jar xxx-microservice.jar --ENV_PROFILE=uat --LIFE_CIRCLE=dev --server.port=11111 --eureka.instance.ip-address=${NODE_IP} --eureka.instance.non-secure-port=${NODE_PORT_11111} --jasypt.encryptor.password=xxx -Dsun.net.inetaddr.ttl=3

由此,我心中也有了疑问:

  • 为什么 Program 参数的写法是 –
  • 假如都是配置server.portVM 参数Program 参数的优先级是?
  • Spring Boot是如何处理两者的呢?

VM args 和 Program args 有什么区别

如何启动一个Java的应用?首先我们可以在Oracle的文档中找到答案。

java [options] -jar filename [args]

options:Command-line options separated by spaces. See Options.

args: The arguments passed to the main() method separated by spaces.

JVM提供了Standard OptionsNon-Standard Options 等6种不同的Options用作不同的场景。其中Standard Options是最常见的Options,例如 -jar filename-version-help 等Options。-Dproperty=value 也是Standard Options,文档中的用法解释是:

Sets a system property value. The property variable is a string with no spaces that represents the name of the property. The value variable is a string that represents the value of the property. If value is a string with spaces, then enclose it in quotation marks (for example -Dfoo=”foo bar”).

设置一个系统属性值。属性变量是一个没有空格的字符串,代表属性的名称。value变量是一个表示属性值的字符串。如果value是一个带空格的字符串,则用引号括起来(例如-Dfoo=”foo bar”)

对此,明确了一点,类似-Dkey=valueVM参数最后会成为System properties

可以看到,在文档中并没有规定 args 的写法,那么为什么在案例中需要在参数前加上--呢。由于启动的是一个Spring Boot项目,尝试去Spring Boot文档中寻找答案。
4.2.2. Accessing Command Line Properties中有说明,Spring Boot的参数均以-- 开头,成为 Command Line Properties,作为外部化参数的一种。说到外部化参数,那是不是Spring Boot还有其他的外部化参数?
Spring Boot 允许外部化配置,以便在不同的环境中使用相同的应用程序代码。可以使用属性文件、 YAML 文件、环境变量和命令行参数来外部化配置,也可以使用@value注释将属性值直接注入 bean,可以通过 Spring 的 Environment 抽象访问属性值,也可以通过@configurationproperties绑定到结构化对象。
对于这么多的外部化配置,就会存在相同的属性覆盖的情况,而Spring Boot按照17种不同的外部化配置规约了不同的优先级,这里引用和本篇文章相关的参数。

4.Command line arguments
9.Java System properties(System.getProperties()).
10.OS environment variables.
15.Application properties packaged inside your jar (application.properties and YAML variants).
17.Default properties (specified by setting SpringApplication.setDefaultProperties).

到这边答案就出来了,对于java -Dkey=value -jar springboot.jar --key=value这句启动命令:

  • 前者-Dkey=value会将key:value键值对写入System properties
  • 后者--key=value属于Spring Boot特定的Command line arguments
  • 以上两种参数对Spring Boot而言都是外置化参数,通常情况下,Command line arguments 的优先级显然比 System properties 和 配置文件 的优先级高

举个栗子

我们可以尝试去配置server.port这个参数去验证

  • application.properties:server.port=8088
  • VM options= -Dserver.port=8081
  • Program arguments= –server.port=8082

启动类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.lazyallen.player.command.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;

@SpringBootApplication
public class CommandDemoApplication implements ApplicationRunner {
private static final Logger logger = LoggerFactory.getLogger(CommandDemoApplication.class);

private static final String SERVER_PORT = "server.port";

@Autowired
Environment env;

public static void main(String[] args) {
//Customizing SpringApplication disable command args
// SpringApplication app = new SpringApplication(CommandDemoApplication.class);
// app.setAddCommandLineProperties(false);
// app.run(args);
SpringApplication.run(CommandDemoApplication.class, args);
}


@Override
public void run(ApplicationArguments args) throws Exception {
logger.info("COMMAND ARGS:args:{}",args);
logger.info("COMMAND ARGS:SourceArgs:{}",args.getSourceArgs());
logger.info("COMMAND ARGS:OptionNames:{}",args.getOptionNames());
logger.info("COMMAND ARGS:NonOptionArgs:{}",args.getNonOptionArgs());
logger.info("COMMAND ARGS:OptionValues for server.port :{}",args.getOptionValues(SERVER_PORT));
logger.info("---------------------------------------");
logger.info("SYSTEM PROPERTIES: server.port:{}",System.getProperty(SERVER_PORT));
logger.info("---------------------------------------");
logger.info("SPRING ENV:server.port:{}",env.getProperty(SERVER_PORT));
}
}

这里我们通过实现ApplicationRunner传入的ApplicationArguments args 就是Program arguments,启动后查看日志,可见最后应用使用的port8082

1
2
3
4
5
6
7
8
9
10
11
12
2020-09-17 23:34:02.392  INFO 6774 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8082 (http) with context path ''
2020-09-17 23:34:02.400 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : Started CommandDemoApplication in 1.988 seconds (JVM running for 2.659)
2020-09-17 23:34:02.401 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : COMMAND ARGS:args:org.springframework.boot.DefaultApplicationArguments@456abb66
2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : COMMAND ARGS:SourceArgs:--server.port=8082
2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : COMMAND ARGS:OptionNames:[server.port]
2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : COMMAND ARGS:NonOptionArgs:[]
2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : COMMAND ARGS:OptionValues for server.port :[8082]
2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : ---------------------------------------
2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : SYSTEM PROPERTIES: server.port:8081
2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : ---------------------------------------
2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : SPRING ENV:server.port:8082
2020-09-17 23:34:06.942 INFO 6774 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'

Think more…

如果仔细看开头的案例,会发现-Dsun.net.inetaddr.ttl=3是作为Program arguments 传入应用的,那么这个参数会生效吗?按以上的说法,Spring Boot虽然可以接收这样的Program arguments,但由于其使用的是 -D 的写法,Spring Boot应该是不认识这个参数的。那么如果传入的是--sun.net.inetaddr.ttl=3会不会生效呢,笔者通过实验,发现的确通过获取Spring的环境参数,的确能获取到这个值,但从System properties 中则是null的。在文档中,对于sun.net.inetaddr.ttl的描述是This is a sun private system property,这是一个私有的System properties,笔者认为虽然能从Spring 的环境参数中获取到这个值,但这个值应该是不会生效的,因为在System properties中这个值依旧是null。

所以,不能简单的认为--key=value-Dkey=value是差不多的,在Spring Boot中,除了优先级不同之外,--key=value并不能去变相的代替-Dkey=value,比如说企图在Program arguments中传入类似 --gc=xxx 的GC参数的骚操作肯定是不行的。

那么问题又来了,为什么--server.port就可以去代替并覆盖-Dserver.port呢?解答这个问题,我们可以尝试去找出,Spring Boot内置web容器中的port是从哪里来的?

Spring Boot是如何处理 –args 参数的

通过Debug发现入口在SpringApplicationrun()方法中,这里我们只展示关键入口和生效步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

public ConfigurableApplicationContext run(String... args) {
...
//在启动时,根据Program arguments 构造出DefaultApplicationArguments
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//准备环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
...
}

protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
MutablePropertySources sources = environment.getPropertySources();
if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
//可以看到,defaultProperties是放在最后的,优先级最低
sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
}
//this.addCommandLineProperties默认为true,但也可以配置为false,这时command line 就会失效
if (this.addCommandLineProperties && args.length > 0) {
String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
if (sources.contains(name)) {
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(
new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
composite.addPropertySource(source);
sources.replace(name, composite);
}
else {
sources.addFirst(new SimpleCommandLinePropertySource(args));
}
}
}

public SimpleCommandLinePropertySource(String... args) {
//可以看到SimpleCommandLineArgsParser是用来解析command line 的
super(new SimpleCommandLineArgsParser().parse(args));
}

//解析command line的逻辑
public CommandLineArgs parse(String... args) {
CommandLineArgs commandLineArgs = new CommandLineArgs();
for (String arg : args) {
if (arg.startsWith("--")) {
String optionText = arg.substring(2);
String optionName;
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;
}

大概流程是,在应用启动时,Spring Boot会根据默认的规则去解析command line,构造成SimpleCommandLinePropertySource,最后加入Spring EnvironmentpropertySources 中。这里的PropertySource很重要,PropertySource 可以简单理解为配置数据源的抽象,上文所讲的各种外部配置都可以看作为数据源。
2020-09-18 at 12.35 A

Tomcat的port是从哪里获取的

当启动Spring Web Application时,控制台通常都会打印出当前容器的port端口。例如:o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8082 (http) ,所以可以直接从TomcatWebServer这个类入手。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void initialize() throws WebServerException {
//从getPortsDescription(false)获得port
logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
...
}

public int getPort() {
// 可以看到是从protocolHandler中拿出来的,实际通常是Http11NioProtocol,而它又委托给了AbstractEndpoint
if (protocolHandler instanceof AbstractProtocol<?>) {
return ((AbstractProtocol<?>) protocolHandler).getPort();
}
// Fall back for custom protocol handlers not based on AbstractProtocol
Object port = getProperty("port");
if (port instanceof Integer) {
return ((Integer) port).intValue();
}
// Usually means an invalid protocol has been configured
return -1;
}

/**
* Server socket port.
*/
private int port = -1;
public int getPort() { return port; }
public void setPort(int port ) { this.port=port; }

可以看到这里的port的默认值是-1,在Spring Boot中,当你想维持一个WebApplicationContext,但又不想处理任何连接,就可以通过将port设置为-1去实现。看到这里笔者发现并没有其他地方引用了setPort()这个方法去设置这个值,这里有个技巧是可以在setPort()上打个断点,然后通过调用栈查看调用的上下文。发现这个参数是从ServerProperties中获取的。
2020-09-18 at 8.09 P

Spring Boot的配置绑定

ServerProperties 从字面含义是服务参数配置,这个类存在于Spring Boot autoconfigure 项目中,在这个项目中有许多其他的Properties类。Spring Boot特性之一就是为了简化开发,为此提供了一系列的通用配置给到开发者配置,真正做到开箱即用,在Common Application properties可以查看具体配置列表。
到这里,我们只要关注的问题就变成了,ServerPropertiesport参数是如何设置的,答案是Spring Boot的配置绑定过程。Spring Boot提供了一系列的参数配置,开发者只需要简单在配置文件中配置一行配置,这行配置的含义就可以生效,肯定其中Spring Boot在背后做了一些事情,而这个过程就是配置绑定。通俗的可以认为,这个过程就是Spring Boot帮你把配置文件或其他外部化配置的参数 一一映射绑定到 对应的Properties中,比如server.port对应的就是ServerPropertiesport参数。这里笔者简要分析入口和关键部分逻辑,感兴趣可以自行Debug。
首先在ConfigurationProperties的注解上可以猜到入口是ConfigurationPropertiesBindingPostProcessorConfigurationPropertiesBindingPostProcessor实现了BeanPostProcessor接口,重写了postProcessBeforeInitialization(),这个方法就是配置绑定的入口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
return bean;
}

private Object bindDataObject(ConfigurationPropertyName name, Bindable<?> target, BindHandler handler,
Context context, boolean allowRecursiveBinding) {
if (isUnbindableBean(name, target, context)) {
return null;
}
Class<?> type = target.getType().resolve(Object.class);
if (!allowRecursiveBinding && context.isBindingDataObject(type)) {
return null;
}
//函数式接口,延迟执行
DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName),
propertyTarget, handler, context, false, false);
return context.withDataObject(type, () -> {
for (DataObjectBinder dataObjectBinder : this.dataObjectBinders) {
// 真正执行的逻辑,可以看到上面的函数也传入进来了,这里
Object instance = dataObjectBinder.bind(name, target, context, propertyBinder);
if (instance != null) {
return instance;
}
}
return null;
});
}

private <T> boolean bind(BeanSupplier<T> beanSupplier, DataObjectPropertyBinder propertyBinder,
BeanProperty property) {
String propertyName = property.getName();
ResolvableType type = property.getType();
Supplier<Object> value = property.getValue(beanSupplier);
Annotation[] annotations = property.getAnnotations();
//真正执行绑定的逻辑
Object bound = propertyBinder.bindProperty(propertyName,
Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations));
if (bound == null) {
return false;
}
if (property.isSettable()) {
property.setValue(beanSupplier, bound);
}
else if (value == null || !bound.equals(value.get())) {
throw new IllegalStateException("No setter found for property: " + property.getName());
}
return true;
}

DataObjectBinder接口有两个实现,其中跟进到JavaBeanBinder#bind()方法中,这里的逻辑不通过Debug根本发现不了执行逻辑。通过Debug发现在bind()方法中传入的propertyBinder中间接持有PropertySources参数,走到这边大概能猜到其实配置绑定的数据源就是PropertySources,会根据优先级从PropertySources中设置对应的配置参数。在引用中有一篇更为详细的Debug博客值得阅读。
2020-09-18 at 8.42 P

总结

通过以上,可以确定几点:

  • Spring Boot在应用启动时会维护一个PropertySources 变量存储所有外部化配置的信息;
  • 同时在配置绑定的过程中,会从PropertySources 中根据优先级获取对应的配置参数,作为有效的配置,最后绑定在*Properties类的属性。
  • 内嵌的tomcat容器在启动时,port信息也是从ServerProperties中获取的,理所当然使用的也是根据优先级顺序得到的有效port值。

引用