Spring cloud application startup process analysis

Posted by neuromancer on Wed, 12 Jan 2022 13:08:13 +0100

  spring cloud must be familiar to everyone. It provides developers with tools to quickly build some common patterns in distributed systems, such as configuration management, service discovery, circuit breaker and load balancing. As the saying goes, if you want to be good at something, you must first use your tools. Today, let's disassemble the workflow of spring cloud (based on spring-cloud-Greenwich.SR2).

   it should be pointed out that the spring cloud is built based on SpringBoot. Reading this article requires young partners to understand the workflow of SpringBoot. Unfamiliar young partners can take a look at the following two articles to obtain some pre knowledge.

  1. Analysis of SpringBoot application startup process
  2. Analysis of SpringBoot automatic assembly mechanism

From bootstrap YML speaking

  except application YML, the spring cloud application can also provide an additional configuration file called bootstrap yml. You usually configure the service name, registry and the address of the configuration center here. Who loads it?

# Intercepted from spring cloud commons reference doc, chapter 1.1
TThe bootstrap context uses a different convention for locating external configuration than the main 
application context. Instead of application.yml (or .properties), you can use bootstrap.yml, keeping
the external configuration for bootstrap and main context nicely separate. 

The spring cloud documentation mentions that bootstrap YML is loaded by the bootstrap context, using bootstrap YML can well separate the external configuration for boot from the main context. The main context here is naturally the application context created when the SpringBoot application starts. What is the bootstrap context?

# Intercepted from spring cloud commons reference doc, chapter 1.1
A Spring Cloud application operates by creating a "bootstrap" context, which is a parent context for
the main application. This context is responsible for loading configuration properties from the external 
sources and for decrypting properties in the local external configuration files. 

It is also mentioned in the document that the spring cloud application will create a bootstrap context at startup and use it as the parent container of the main context. Bootstrap context has two main functions:

  1. Load configuration items from external
  2. Decrypt the properties in the local external configuration file

The second point may not be so common. The first point will be clearer to put it another way - pulling data from the configuration center. What must be here? We already have a concept. Why? That is, how to achieve it? Don't worry, look down.

bootstrap context

  the above figure is our starting point - bootstrap applicationlistener. Let's see its definition:

// Don't ask me how I got here
public class BootstrapApplicationListener
		implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
	// omitted...		
}

Firstly, it is an applicationlister. Secondly, the event it listens to is ApplicationEnvironmentPreparedEvent. Previous analysis SpringBoot application startup process We said that the event was sent when the Environment has been initialized but the main context has not been created. Well, since it's a listener, look at its handling of events.

onApplicationEvent(...)

@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
    // The Environment created when the SpringBoot application starts
    ConfigurableEnvironment environment = event.getEnvironment();
    // First check whether this feature is on
    if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class, true)) {
        return;
    }

    // Import: re entry prevention
    // The Spring auto assembly mechanism does not support assigning an ApplicationListener to a separate ApplicationContext
    // Therefore, this applicationlister will be called back when creating bootstrap context and main context
    // Obviously, the time node we are interested in is when we create the main context
    if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
        return;
    }

    ConfigurableApplicationContext context = null;
    // Bootstrap is only the default name of the configuration file used by bootstrap context
    // You can specify it in System Properties, such as - dspring cloud. bootstrap. name=some-file-name
    String configName = environment.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");

    // Check whether the developer intends to set the parent container for the main context. Generally, there is no parent container
    for (ApplicationContextInitializer<?> initializer : event.getSpringApplication().getInitializers()) {
        // The ParentContextApplicationContextInitializer is specifically responsible for setting up the parent container
        if (initializer instanceof ParentContextApplicationContextInitializer) {
            // If yes, the parent container is returned if its id is bootstrap
            // Otherwise, return the parent container of the parent container, that is, maincontext getParent(). getParent()
            context = findBootstrapContext(
                    (ParentContextApplicationContextInitializer) initializer, configName);
        }
    }

    // The developer did not configure the bootstrap context by himself
    if (context == null) {
        // Then create it and set it as the parent container of main context
        context = bootstrapServiceContext(environment, event.getSpringApplication(), configName);
        // Register the listener for the main context and close the bootstrap context when its startup fails
        event.getSpringApplication().addListeners(new CloseContextOnFailureApplicationListener(context));
    }
}

Analyze onApplicationEvent(...) You can know that the name of the configuration file used by bootstrap context can be specified arbitrarily. The default is bootstrap. Note that this name cannot be used in application Specified in YML because of application YML is loaded by main context (if you are interested in this process, you can see ConfigFileApplicationListener). In general, we will not configure the ParentContextApplicationContextInitializer to set the parent container for the main context. The highlight naturally falls on the bootstrap servicecontext (...) Yes.

bootstrapServiceContext(...)

private ConfigurableApplicationContext bootstrapServiceContext(
        ConfigurableEnvironment environment, final SpringApplication application, String configName) {
    // Create a new Environment and register the two propertysources systemEnvironment and systemProperties by default
    StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
    // Clear the default registered PropertySource to get a completely blank Environment
    MutablePropertySources bootstrapProperties = bootstrapEnvironment.getPropertySources();
    for (PropertySource<?> source : bootstrapProperties) {
        bootstrapProperties.remove(source.getName());
    }

    // Assemble bootstrap property source
    // This step is to give the ConfigFileApplicationListener to read
    // bootstrap. Contents in YML (properties)
    Map<String, Object> bootstrapMap = new HashMap<>();
    // ${spring.cloud.bootstrap.name:bootstrap}
    bootstrapMap.put("spring.config.name", configName);
    // Just use the most basic type for bootstrap context
    bootstrapMap.put("spring.main.web-application-type", "none");

    // bootstrap. The path of YML is also configurable. It is empty by default, that is, the root directory of resources
    String configLocation = environment.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
    if (StringUtils.hasText(configLocation)) {
        bootstrapMap.put("spring.config.location", configLocation);
    }
    // Add a bootstrap property source, preceded by onApplicationEvent(...) It's used to prevent re-entry
    bootstrapProperties.addFirst(new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));

    // Synchronize the data in the original Environment to the newly created Environment
    for (PropertySource<?> source : environment.getPropertySources()) {
        // StubPropertySource is used for placeholders and does not contain real data
        // In this section, you can see the startup process of WebApplicationContext
        // The ServletContextPropertySource is occupied first and then replaced
        if (source instanceof PropertySource.StubPropertySource) {
            continue;
        }
        bootstrapProperties.addLast(source);
    }

    // bootstrap context is a background context
    // Some configuration items need to be adjusted, such as not printing Banner
    // There is no need to use ApplicationContext that can start WebServer
    SpringApplicationBuilder builder = new SpringApplicationBuilder()
            .profiles(environment.getActiveProfiles())
            .bannerMode(Banner.Mode.OFF)
            .environment(bootstrapEnvironment)
            // Don't use the default properties in this builder
            .registerShutdownHook(false)
            .logStartupInfo(false)
            .web(WebApplicationType.NONE);
    final SpringApplication builderApplication = builder.application();
    // In the case of deployment using war package, mainApplicationClass cannot be inferred
    if (builderApplication.getMainApplicationClass() == null) {
        builder.main(application.getMainApplicationClass());
    }
    
    // If context refresh is in progress
    // The context refresh mentioned here is related to RefreshScope
    // Not ApplicationContext#refresh() 
    // We will talk about this part in the next article
    if (environment.getPropertySources().contains("refreshArgs")) {
        // As for the log listener, it affects the global state
        // However, when doing context refresh, you just want to refresh the Environment
        // So we need to filter out this kind of listener
        builderApplication.setListeners(filterListeners(builderApplication.getListeners()));
    }
    
    // It is allowed to use SPI mechanism to specify configuration class for bootstrap context separately
    builder.sources(BootstrapImportSelectorConfiguration.class);
    // Create a bootstrap context and go through the automatic assembly process
    // Go to ConfigFileApplicationListener and read the bootstrap Content of YML
    // Go to PropertySourceLocator and load external configuration items
    final ConfigurableApplicationContext context = builder.run();
    // Specify its id as bootstrap
    context.setId("bootstrap");
    // Listen to the creation of main context and set the bootstrap context as its parent container
    addAncestorInitializer(application, context);
    // After the bootstrap context is created, these configuration items are useless
    bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
    // Because the bootstrap context is likely to read new configuration items
    // Synchronize these new data to the Environment of the main context
    mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
    return context;
}

Don't panic. Although the method is long, it doesn't do much:

  1. Create a bootstrap context and set it as the parent container of the main context
  2. The configuration class can be specified separately for bootstrap context
  3. Synchronize the data in the bootstrap environment and the main environment

  the first one added is called bootstrap_ PROPERTY_ SOURCE_ The PropertySource of name is to help ConfigFileApplicationListener locate and read bootstrap Configuration item in YML (properties). Since this part is not the focus of this article, please read its source code yourself.

  the second step is to create the bootstrap context. Note that this step reuses the startup process of the SpringBoot application. Everything such as automatic configuration will be arranged for the bootstrap context. Since the auto assembly mechanism is not accurate enough to specify that an auto configuration class can only be used for an ApplicationContext, special attention should be paid to the reentry of components such as ApplicationListener and ApplicationContextInitializer. Spoilers in advance. Loading external configuration items is implemented as ApplicationContextInitializer. We will talk about this topic in the next section. Now we only need to know that the builder is executed While creating the bootstrap context, run () will also load the external configuration items through PropertySourceLocator.

  what if you want to introduce some configuration classes to bootstrap context separately? This question can be passed Spring SPI mechanism To solve the problem, I believe you will understand by looking at the source code of bootstrap importselectorconfiguration. I won't say more.

  finally, synchronize the data in the bootstrap environment and the main environment. Many external configuration items may have been loaded during the creation of the bootstrap context, and the application code cannot be directly injected into the bootstrap environment. If you want to use these configuration items, you can only merge them into the main environment.

private void mergeDefaultProperties(MutablePropertySources environment,
                                    MutablePropertySources bootstrap) {
    // Sync DEFAULT_PROPERTIES, space constraints, deleted here
    
	mergeAdditionalPropertySources(environment, bootstrap);
}

private void mergeAdditionalPropertySources(MutablePropertySources environment,
                                            MutablePropertySources bootstrap) {
    // Set the default in the main environment_ Properties take it out
    PropertySource<?> defaultProperties = environment.get(DEFAULT_PROPERTIES);
    // Repackage as ExtendedDefaultPropertySource
    ExtendedDefaultPropertySource result = defaultProperties instanceof ExtendedDefaultPropertySource
        ? (ExtendedDefaultPropertySource) defaultProperties
        : new ExtendedDefaultPropertySource(DEFAULT_PROPERTIES,
                                            defaultProperties);
    // Traverse the data in the bootstrap environment
    for (PropertySource<?> source : bootstrap) {
        // If it is the main environment, no
        if (!environment.contains(source.getName())) {
            // If so, you can access these external configurations through the main environment
            result.add(source);
        }
    }
    // The following is to ensure that the data structures in the two environment s are consistent
    for (String name : result.getPropertySourceNames()) {
        bootstrap.remove(name);
    }
    addOrReplace(environment, result);
    addOrReplace(bootstrap, result);
}

After that, you can access the external configuration items loaded by the bootstrap context through the main environment. Finally, take a look at the ExtendedDefaultPropertySource:

private static class ExtendedDefaultPropertySource extends SystemEnvironmentPropertySource {

  // rest are omitted...

   @Override
   public Object getProperty(String name) {
       // Read the external configuration item first
      if (this.sources.containsProperty(name)) {
         return this.sources.getProperty(name);
      }
       // Then read the data loaded by the main context
      return super.getProperty(name);
   }

   @Override
   public boolean containsProperty(String name) {
       // Similarly, first judge whether it exists in the external configuration item
      if (this.sources.containsProperty(name)) {
         return true;
      }
       // Think about yourself
      return super.containsProperty(name);
   }
}

As you can see, a clear access order is defined here: external configuration items have priority over local configuration items.

PropertySourceLocator

/**
 * Strategy for locating (possibly remote) property sources for the Environment.
 * Implementations should not fail unless they intend to prevent the application from
 * starting.
 */
public interface PropertySourceLocator {
   PropertySource<?> locate(Environment environment);
}

Obviously, this is an extension interface, which is left to developers to implement. Spring cloud itself is only responsible for calling. It is easy to know that the calling logic is implemented in propertysourcebootstrap configuration. ok, let's go.

// Look at spring Factories can know that this configuration class is specially assigned to the bootstrap context load
public class PropertySourceBootstrapConfiguration implements
		ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
    
    @Autowired(required = false)
	private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
		
    // rest are omitted...
}

It is an ApplicationContextInitializer. Well, you can directly look at the corresponding interface implementation.

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
    // Wrapper class to hold all configurations loaded by PropertySourceLocator
    CompositePropertySource composite = new CompositePropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME);
    // Order first
    AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
    boolean empty = true;
    // This environment is the bootstrap environment
    ConfigurableEnvironment environment = applicationContext.getEnvironment();
    // Traverse PropertySourceLocator
    for (PropertySourceLocator locator : this.propertySourceLocators) {
        PropertySource<?> source = null;
        // Get external configuration one by one
        source = locator.locate(environment);
        if (source == null) {
            continue;
        }
        // If so, merge them together
        logger.info("Located property source: " + source);
        composite.addPropertySource(source);
        empty = false;
    }
    // If external configuration is loaded
    if (!empty) {
        MutablePropertySources propertySources = environment.getPropertySources();
        String logConfig = environment.resolvePlaceholders("${logging.config:}");
        LogFile logFile = LogFile.get(environment);
        // Remove old
        if (propertySources.contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
            propertySources.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
        }
        // Add a new one. Here, you can adjust the order of adding according to the configuration, so as to affect the priority
        insertPropertySources(propertySources, composite);
        // Reload the log component if the log configuration file changes
        reinitializeLoggingSystem(environment, logConfig, logFile);
        // According to the configuration item logging Level resets the log level of the corresponding package
        setLogLevels(applicationContext, environment);
        // According to the configuration item spring profiles. Include reloads additional configuration files
        handleIncludedProfiles(environment);
    }
}

The main logic is easy to understand, but many details need to be considered. For example, if the log configuration file changes, the log component needs to be reinitialized; Another example is spring profiles. The include configuration item has changed. You need to load these additional configuration files. These details are left to you. Finally, if you use Zookeeper or Nacos as the configuration center, you can see their implementation of PropertySourceLocator.

End

  today I shared the bootstrap context startup process with you. I believe you will gain something after reading it. Next time, let's talk about RefreshScope, which is a very interesting feature. End.

Topics: Java Spring Spring Boot Spring Cloud