This article mainly introduces the Spring extension mechanism based on Apollo (architecture shown in the figure above, no more details) and the implementation method of Spring. The implementation method of Apollo is not complicated, and it makes good use of Spring’s rich extension mechanism to build a real-time powerful configuration center

  • How do I plug in the Apollo configuration

Yml (.properties) on the server. The advantage of Apollo is to use its centralized characteristics to achieve unified configuration and real-time pull, which can not only reduce the chance of repeated configuration and error, but also can achieve real-time effect for some dynamic changes of the property. Especially for keys like SecretKey that have dynamically changing requirements. Conditionalxxx = Conditionalxxx, Conditionalxxx = Conditionalxxx, Conditionalxxx = Conditionalxxx, Conditionalxxx = Conditionalxxx, Conditionalxxx = Conditionalxxx, Conditionalxxx = Conditionalxxx, Conditionalxxx = Conditionalxxx

Spring allows you to start with extended Spring startup classes called SpringApplications. Examples include initializers and Listeners (both introduced via spring.factories). Initializers for ApplicationContextInitializer (ApplicationContext initializer), listeners for ApplicationListener (i.e. Application is up and running in the process of all kinds of things SpringApplicationEvent, ApplicationContextEvent, Environment events, etc.

public SpringApplication(ResourceLoader resourceLoader, Class<? >... primarySources) { ...setInitializers((Collection) getSpringFactoriesInstances(
			ApplicationContextInitializer.class));
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); . }Copy the code

During Spring startup, i.e. springApplication.run (), the Environment is initialized first (this time mainly consists of the command line and system Environment variables at startup) as follows

sources.addFirst(new SimpleCommandLinePropertySource(args)); . propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties())); propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));Copy the code

This is the initial Environment, a variety of other Environment is through the above description of ApplicationListener injection, here is not the details of the listener, the look at the names of the ConfigFileApplicationListener, It listens ApplicationEnvironmentPreparedEvent, event trigger calls after registered EnvironmentPostProcessor to expand in the Environment (if we are to provide third-party jar, The Environment inside the JAR needs to be exposed, by extending EnvironmentPostProcessor); And Spring Cloud BootstrapApplicationListener, it extends through the listener mechanisms more content, ConfigFileApplicationListener event triggering method is illustrated below:

After the Environment is ready, Spring creates the ApplicationContext (ApplicationContext, as the name implies, is the context of the current application, The main combination DefaultListableBeanFactory AnnotationConfigServletWebServerApplicationContext by default,, DefaultListableBeanFactory is mainly used to register all bean definitions), then the prepareContext initializers () to register before implementation:

ConfigurableEnvironment environment = context.getEnvironment();
String enabled = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "false");
if(! Boolean.valueOf(enabled)) {return;
}

if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
  //already initialized
  return;
}

String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);

CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
for(String namespace : namespaceList) { Config config = `ConfigService.getConfig(namespace); ` composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config)); } `environment.getPropertySources().addFirst(composite); `Copy the code

As shown in the code above, initializer works if Apoll.bootstrap. enabled is true, Add a CompositePropertySource to the Environment (the properties provided by this PropertySource are pulled synchronically through ConfigService.getConfig), It is placed at the first place in the PropertySource list (Environment calls PropertySource when it gets the property, so the Configuration of Apollo is all pulled to the local file and application process (assuming the network is ok). The Apollo configuration takes effect during Spring’s subsequent Bean load initialization (if not in native mode, Apollo will automatically pull the configuration center configuration via RemoteConfigRepository by default, bingo!)

If Apollo. Bootstrap. enabled is false, how does Apollo play? Apollo provides the @enableApolloConfig annotation to enable configuration fetch (note that the configuration does not take effect before the Bean is loaded, and the Condition created by the Bean does not get configuration attributes). This approach mainly uses Spring’s Bean load initialization extension mechanism. Let’s see how this extends:

@Configuration
@EnableApolloConfig
public class AppConfig {
  @Bean
  public TestJavaConfigBean javaConfigBean() {
    returnnew TestJavaConfigBean(); }}Copy the code

@enableApolloConfig has to be configured with @Configuration, otherwise it won’t work. This is mainly a reference to Spring’s ConfigurationClass initialization mechanism. See the breakdown below.

Let’s start with the two most critical classes, The BeanFactoryPostProcessor (allows for custom modification of an Application Context’s bean) has been mentioned many times in Spring Boot series Definitions, A BeanFactoryPostProcessor may interact with and modify bean definitions, But never bean instances) and BeanPostProcessor (Factory hook that allows for custom modification of new bean instances,e.g Checking for marker interfaces or wrapping them with proxies).

In the aforementioned AnnotationConfigServletWebServerApplicationContext (that is, the Spring The Boot default ApplicationContext) inside a AnnotatedBeanDefinitionReader properties, AnnotatedBeanDefinitionReader to register the BeanFactory spring BeanFactoryPostProcessor and BeanPostProcessor, The ConfigurationClassPostProcessor is mainly responsible for ConfigurationClass parsing (informs the for bootstrapping processing of {@ the link Configuration @Configuration} classes), which is the entry point for Bean loading. Various BeanFactoryProcessor and BeanPostProcessor will be added in the subsequent processing process for extension (we can customize them if necessary), as follows:

if(! registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) { RootBeanDefinition def = new RootBeanDefinition(`ConfigurationClassPostProcessor.class`); def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
}
Copy the code

ConfigurationClassPostProcessor did the following work:

protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
    // Recursively process any member (nested) classes first
    processMemberClasses(configClass, sourceClass);
    
    // Process any @PropertySource annotations
    ...
    
    // Process any @ComponentScan annotations
    Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
    		sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    if(! componentScans.isEmpty() && ! this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
    	for (AnnotationAttributes componentScan : componentScans) {
    		// The config class is annotated with @ComponentScan -> perform the scan immediately
    		Set<BeanDefinitionHolder> scannedBeanDefinitions =
    				this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
    		// Check the set of scanned definitions for any further config classes and parse recursively if needed
    		for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
    			BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
    			if (bdCand == null) {
    				bdCand = holder.getBeanDefinition();
    			}
    			if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
    				parse(bdCand.getBeanClassName(), holder.getBeanName());
    			}
    		}
    	}
    }
    
    // Process any @Import annotations
    processImports(configClass, sourceClass, getImports(sourceClass), true);
    
    // Process any @ImportResource annotations
    ...
    
    // Process individual @Bean methods
    Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
    for (MethodMetadata methodMetadata : beanMethods) {
    	configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
    }
    
    // Process default methods on interfaces
    processInterfaces(configClass, sourceClass);
    
    // Process superclass, if any
    ...
Copy the code

How does Apollo deal with this extension mechanism?

In normal development, our custom Bean always takes effect first. Spring Boot’s various Starter beans are often loaded to determine whether the Bean has been defined, such as DataSource Bean, which requires priority processing in design. Spring in ConfigurationClass will determine whether the parse for DeferredImportSelector and ImportBeanDefinitionRegistrar type, if it is, will be first on the List, behind to deal with again, Visible above deferredImportSelectors and loadBeanDefinitionsFromRegistrars below the code.

private void loadBeanDefinitionsForConfigurationClass(
    	ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
    ...
    if (configClass.isImported()) {
    	registerBeanDefinitionForImportedConfigurationClass(configClass);
    }
    for (BeanMethod beanMethod : configClass.getBeanMethods()) {
    	loadBeanDefinitionsForBeanMethod(beanMethod);
    }
    
    loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
    loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
Copy the code

Get down to business, we focus on the Apollo through ImportBeanDefinitionRegistrar add several processor:

  1. PropertySourcesProcessor


    Further calls initializePropertySources, the effect is similar to previous iniitializer.

private void initializePropertySources() {... CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME); //sort by order asc ImmutableSortedSet<Integer> orders = ImmutableSortedSet.copyOf(NAMESPACE_NAMES.keySet()); Iterator<Integer> iterator = orders.iterator();while (iterator.hasNext()) {
      int order = iterator.next();
      for (String namespace : NAMESPACE_NAMES.get(order)) {
        Config config = ConfigService.getConfig(namespace);
    
        composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
      }
    }
    
    // add after the bootstrap property source or to the first
    if (environment.getPropertySources()
        .contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
      environment.getPropertySources()
          .addAfter(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME, composite);
    } else{ environment.getPropertySources().addFirst(composite); }}Copy the code
  1. ApolloAnnotationProcessor


    Which processMethod () handles the @ ApolloConfigChangeListener, ApolloConfigChangeListener used to configure center configuration changes trigger attribute value to modify & Bean refresh.

    Visible ApolloAnnotationProcessor inherited BeanPostProcessor, BeanPostProcessor is applied, when the Bean is initialized after initialization before opening to the outside interface, much the way, Beanfactory.getbean () generates and initializes Bean instances, excluding special beans such as BeanFactoryPostProcessor and BeanPostProcessor. Bean instance initialization is the ApplicationContext. The refresh () calls when the BeanFactory) getBean ().

  2. SpringValueProcessor

    SpringValueProcessor also inherits ApolloProcessor and implements the BeanFactoryPostProcessor interface, which should have two overloaded methods

    Image below processField () in the above super. PostProcessBeforeInitialization () is called, when the function is resolved with @ the Value in the Bean Field, registered to the map, Convenient real-time refresh after subsequent pull the latest configuration Bean attribute values, and processMethod () and processBeanPropertyValues (), this method is a little special, is to deal with Bean TypedStringValue type attributes, And need to act with 4), similar.

  3. SpringValueDefinitionProcessor


    BeanDefinitionRegistryPostProcessor SpringValueDefinitionProcessor is, in order to put Bean and TypedStringValue type attribute value in the map, the follow-up from 3 to updated in real time.

  • Last question, how is the Apollo configuration updated in real time?

    RemoteConfigRepository is the repository that pulls the configuration center configuration. If the configuration changes, the RepositoryChangeListener triggers the ConfigChangeListener. AutoUpdateConfigChangeListener is responsible for the automatic update of Bean properties change, we can also customize ConfigChangeListener, to refresh the particular Bean, The following figure shows the synchronous configuration of RemoteConfigRepository:

    See below the AutoUpdateConfigChangeListener onChange logic code. The springValueRegistry is the same object as the springValueRegistry in the SpringValueProcessor mentioned above.

@Override
public void onChange(ConfigChangeEvent changeEvent) {
    ...
    for (String key : keys) {
      // 1. check whether the changed key is relevant
      Collection<SpringValue> targetValues = `springValueRegistry`.get(key);
      if (targetValues == null || targetValues.isEmpty()) {
        continue;
      }
    
      // 2. check whether the value is really changed or not (since spring property sources have hierarchies)
      if(! shouldTriggerAutoUpdate(changeEvent, key)) {continue;
      }
    
      // 3. update the value
      for(SpringValue val : targetValues) { updateSpringValue(val); }}}Copy the code

@Configuration
@ConfigurationProperties(prefix = "spring.datasource") @RefreshScope public class MetadataDataSourceConfig { private String url; private String username; . @Bean(name ="masterDataSource")
    @RefreshScope
    public DataSource masterDataSource() { DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(this.url); . } @ApolloConfigChangeListener public void onChange(ConfigChangeEvent changeEvent) { refreshScope.refresh("masterDataSource");
    }
Copy the code

Historical articles:

  • How does Zookeeper ensure sequence consistency
  • Do you really understand volatile
  • Redis Scan algorithm design idea
  • Microservice session crashes
  • Moving from dynamic proxy implementation to Spring AOP is enough
  • How to Quickly get familiar with the Spring Technology Stack
  • Spring Boot series two: One diagram to understand the request processing process
  • Spring Validation implementation principle analysis
  • See the whole Dubbo service reference process in one picture