SpringApplication#run 调用两次

在使用nacos作为配置中心的过程中,debug源码的过程中发现,作为启动类的SpringApplication#run这个方法会调用两次,就很奇怪。

调用两次原理

通过跟踪调试发现,是在BootstrapApplicationListener中再次调用了SpringApplication,相关的代码如下:

1
2
3
4
5
6
7
8
9
10
11
// 构建了一个SpringApplication的构建器,内置了一个SpingApplication对象
SpringApplicationBuilder builder = new SpringApplicationBuilder()
.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
.environment(bootstrapEnvironment)
// Don't use the default properties in this builder
.registerShutdownHook(false).logStartupInfo(false)
.web(WebApplicationType.NONE);
// ....省略代码
// 这里的调用run方法,本质上就是调用了SpringApplication#run的方法
// 所以我们会看到,两次调用run方法。
final ConfigurableApplicationContext context = builder.run();

ps:上面代码方法调用栈:onApplicationEvent->bootstrapServiceContext

构建SpringApplicationBuilder的代码如下:

1
2
3
4
5
6
7
public SpringApplicationBuilder(Class<?>... sources) {
this.application = createSpringApplication(sources);
}

protected SpringApplication createSpringApplication(Class<?>... sources) {
return new SpringApplication(sources);
}

为啥只有spring-cloud会执行两次,而一个普通的springboot没有,根本原因是spring-boot是没有BootstrapApplicationListener这个监听器。这个监听器的配置是在spring-cloud-context这个jar包下的spring.factories,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.cloud.bootstrap.BootstrapApplicationListener,\
org.springframework.cloud.bootstrap.LoggingSystemShutdownListener,\
org.springframework.cloud.context.restart.RestartListener

# Bootstrap components
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,\
org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration,\
org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration

这个监听器的作用是什么呢?我们可以看下这个监听器的注释:

1
2
3
4
5
6
A listener that prepares a SpringApplication (e.g. populating its Environment) by
delegating to {@link ApplicationContextInitializer} beans in a separate bootstrap
context. The bootstrap context is a SpringApplication created from sources defined in
spring.factories as {@link BootstrapConfiguration}, and initialized with external
config taken from "bootstrap.properties" (or yml), instead of the normal
"application.properties".

我理解的大概意思就是,创建一个Context上下文,用来解析定义在spring.factories中定义的资源信息,从bootstrap的配置文件中初始化额外的配置信息,代替正常的配置信息(application.properties)

这个监听器的出现,使我们可以在服务启动之前加载自定义的配置信息,这个配置信息可以是远程的,也可以是本地的。nacos的spring-cloud版本能够实现从远程加载配置信息,就是利用了这个监听器

使用BootstrapApplicationListener加载配置原理

通过上文,我们知道了监听器调用了bootstrapServiceContext方法,加载配置的关键是在这个方法中实现的,下面我们继续解析这个方法,上代码

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
private ConfigurableApplicationContext bootstrapServiceContext(
ConfigurableEnvironment environment, final SpringApplication application,
String configName) {
StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
MutablePropertySources bootstrapProperties = bootstrapEnvironment
.getPropertySources();

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// 加载spring.factories中配置类型为BootstrapConfiguration.class的数据
// names就是配置类的名称的集合
// 此时我们会加载到一个PropertySourceBootstrapConfiguration配置信息
List<String> names = new ArrayList<>(SpringFactoriesLoader
.loadFactoryNames(BootstrapConfiguration.class, classLoader));
for (String name : StringUtils.commaDelimitedListToStringArray(
environment.getProperty("spring.cloud.bootstrap.sources", ""))) {
names.add(name);
}
// 创建SpringApplication
SpringApplicationBuilder builder = new SpringApplicationBuilder()
.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
.environment(bootstrapEnvironment)
.registerShutdownHook(false).logStartupInfo(false)
.web(WebApplicationType.NONE);
// 判断names对应的类是否存在,只保留存在的类
List<Class<?>> sources = new ArrayList<>();
for (String name : names) {
Class<?> cls = ClassUtils.resolveClassName(name, null);
try {
cls.getDeclaredAnnotations();
}
catch (Exception e) {
continue;
}
sources.add(cls);
}
// 这里将sources添加到SpringApplication的实例上
builder.sources(sources.toArray(new Class[sources.size()]));
// 执行run方法之后,会将sources实例化成bean并注入到context中
final ConfigurableApplicationContext context = builder.run();
return context;
}

我们继续回到onApplicationEvent中,生成context之后,调用apply方法将context中的ApplicationContextInitializer类的实现添加到SpringAppliation实例中(注意:这里的实例是第一次调用run方法生成的)

1
2
3
4
5
if (context == null) {
context = bootstrapServiceContext(environment, event.getSpringApplication(),
configName);
}
apply(context, event.getSpringApplication(), environment);

继续看apply方法

1
2
3
4
5
6
7
8
9
10
11
12
private void apply(ConfigurableApplicationContext context,
SpringApplication application, ConfigurableEnvironment environment) {
@SuppressWarnings("rawtypes")
// 从新生成的Context中获取ApplicationContextInitializer实例
List<ApplicationContextInitializer> initializers = getOrderedBeansOfType(context,
ApplicationContextInitializer.class);
// 将ApplicationContextInitializer的实例添加到第一次生成SpringApplication中
// 这里的实例就包括一个PropertySourceBootstrapConfiguration实例
application.addInitializers(initializers
.toArray(new ApplicationContextInitializer[initializers.size()]));
addBootstrapDecryptInitializer(application);
}

执行完毕之后,会继续执行第一次SpringApplication的run方法,

1
2
3
4
5
6
// 调用event事件
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
// 执行ApplicationContextInitializer方法的初始化
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);

可以看到,通过监听器,我们添加了额外的ApplicationContextInitializer实例(PropertySourceBootstrapConfiguration)到SpringApplication中,PropertySourceBootstrapConfiguration类用来加载额外的配置信息,实现自定义配置信息

PropertySourceBootstrapConfiguration解析

如代码所示:

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
@Configuration
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
@Autowired(required = false)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
// 进行初始化操作
for (PropertySourceLocator locator : this.propertySourceLocators) {
// 遍历propertySourceLocators获取配置信息,将获取到的配置信息全部放到
// composite中
PropertySource<?> source = null;
source = locator.locate(environment);
if (source == null) {
continue;
}
logger.info("Located property source: " + source);
composite.addPropertySource(source);
empty = false;
}
if (!empty) {
// 将配置信息放到environment中,后面初始化应该就会使用这里的配置信息
MutablePropertySources propertySources = environment.getPropertySources();
insertPropertySources(propertySources, composite);
}
}
}

这个类有一个属性是PropertySourceLocator的集合,注意该属性上有Autowired注解,所以我们只要实例化PropertySourceLocator就会添加到这里,完成自定义配置的加载。

还记得上文中讲到,bootstrapServiceContext会将BootstrapConfiguration类型的配置实例化到context中,所以我们只要添加对应的配置信息就可以了。nacos的配置如下:

1
2
3
4
5
6
7
8
9
10
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.nacos.NacosConfigAutoConfiguration,\
com.alibaba.cloud.nacos.endpoint.NacosConfigEndpointAutoConfiguration
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.alibaba.cloud.nacos.diagnostics.analyzer.NacosConnectionFailureAnalyzer
org.springframework.boot.env.PropertySourceLoader=\
com.alibaba.cloud.nacos.parser.NacosJsonPropertySourceLoader,\
com.alibaba.cloud.nacos.parser.NacosXmlPropertySourceLoader

ps: 该配置来自spring-cloud-starter-alibaba-nacos-config 2.0.4-release版本