SpringApplication#run 调用两次

682 阅读3分钟

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

调用两次原理

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

// 构建了一个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的代码如下:

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,如下所示:

# 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

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

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方法,加载配置的关键是在这个方法中实现的,下面我们继续解析这个方法,上代码

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方法生成的)

if (context == null) {
    context = bootstrapServiceContext(environment, event.getSpringApplication(),
          configName);
}
apply(context, event.getSpringApplication(), environment);

继续看apply方法

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方法,

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

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

PropertySourceBootstrapConfiguration解析

如代码所示:

@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的配置如下:

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版本