spring+zookeeper uses zk as the configuration center and analyzes the ${} placeholder principle

Usually in spring projects, we scan the configuration file under resources, parse the configuration in the configuration file, and replace the placeholders by invokeBeanFactoryPostProcessors when the container starts.
In the same way, when we need to access the configuration center, in theory, the principle remains the same. We also replace the placeholders when executing the invokeBeanFactoryPostProcessors method. What we need to solve is to parse the configuration by scanning the configuration file and replace it with Read zk and put the configuration in the corresponding place for replacement.

In the xml file, create the bean of PreferencesPlaceholderConfigurer, and its properties attribute refers to the properties loading class. This loading class is the custom class that loads the zk configuration.

<bean id="prop" class="com.dome.config.ZkPropFactoryBean"></bean>
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer">
  <property name="properties" ref="prop"/>
</bean>

When the container is initialized, xml will be loaded. The beans we defined above will be loaded into beanDefinition and placed in beanFactory. When the invokeBeanFactoryPostProcessors method is executed, various BeanFactoryPostProcessors will be filtered from the beanFactory for execution, and the propertyConfigurer bean we defined, his The class path PreferencesPlaceholderConfigurer implements BeanFactoryPostProcessor and PriorityOrdered, so it will also be filtered out.

public static void invokeBeanFactoryPostProcessors(
       ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
           
    ..Most of the irrelevant code is omitted here..
    
    List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
    List<String> orderedPostProcessorNames = new ArrayList<>();
    List<String> nonOrderedPostProcessorNames = new ArrayList<>();
    for (String ppName : postProcessorNames) {
       if (processedBeans.contains(ppName)) {
       }
       else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
           //The propertyConfigurer we defined will be filtered out here, and the bean will be instantiated in advance after filtering out.
           //Furthermore, our customized propertyConfigurer bean, its properties refer to the prop bean, so these two beans will be initialized together in this step.
          priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
       }
       else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
          orderedPostProcessorNames.add(ppName);
       }
       else {
          nonOrderedPostProcessorNames.add(ppName);
       }
    }
    sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
    //This is to replace the placeholder. I will talk about initialization later.
    invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);
    
    ..Most of the irrelevant code is omitted here..
}

The getBean method is too complicated, so I will extract some key code.

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
       throws BeanCreationException {
..Most of the irrelevant code is omitted here..

    // Initialize the bean instance.
    Object exposedObject = bean;
    try {
        //When instantiating a bean, the properties of the bean will be filled in here. The prop we define is the property of the propertyConfigurer, so when creating the propertyConfigurer, the prop will be instantiated first.
       populateBean(beanName, mbd, instanceWrapper);
       exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    catch (Throwable ex) {
       if (ex instanceof BeanCreationException & amp; & amp; beanName.equals(((BeanCreationException) ex).getBeanName())) {
          throw (BeanCreationException) ex;
       }
       else {
          throw new BeanCreationException(
                mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
       }
    }
..Most of the irrelevant code is omitted here..
}

When initializing the bean, the zk bean will also be initialized.

import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.core.io.Resource;
public class ZkPropFactoryBean extends PropertiesFactoryBean implements FactoryBean<Properties>, InitializingBean {
    private Properties properties;
    
   public void setLocations(Resource... locations) {
       //Zk will be initialized and loaded here.
      ZookeeperPropertiesLoader.instance
    }
    //Because the changed bean inherits PropertiesFactoryBean, you will still go here when instantiating the bean.
    protected Properties createProperties() throws IOException {
        //Put all zk configurations obtained by ZkPropLoader.instance.getProperties() into properties.
        //In this case, after the instantiation of ZkPropFactoryBean is completed, all the properties placed in its properties are zk configurations.
    }
}

Here is an enumeration defined to load the configuration of the zk configuration center

public enum ZkPropLoader {
    instance;

    //Connect zk here and put all the configurations in properties
    private Map<String, String> properties = new LinkedHashMap();
}

According to the above bean initialization process, we know
1. The container initializes the propertyConfigurer first, because the properties refer to prop.
2. Instantiate prop. During this process, link zk and put all configurations into its properties.
3. After the above prop is instantiated, the prop will go to factory.getObject()

@Override
@Nullable
public final Properties getObject() throws IOException {
    if (this.singleton) {
        //We know that singleton is true and singletonInstance will load properties when we instantiated it just now
       return this.singletonInstance;
    }
    else {
       return createProperties();
    }
}

4. Therefore, after the propertyConfigurer is loaded, its properties will also be the content loaded by zk.

The above is a series of actions for bean initialization and zk initialization, which can be regarded as preparation work.
The next step is the invokeBeanFactoryPostProcessors method of invokeBeanFactoryPostProcessors. Only then does the placeholder officially begin to be parsed.

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    try {
        //Load the above Properties, which is the obtained zk configuration
       Properties mergedProps = mergeProperties();

       // Convert the merged properties, if necessary.
       convertProperties(mergedProps);

       //Here we will take out the beanDefinition in beanfactory and parse each placeholder
       processProperties(beanFactory, mergedProps);
    }
    catch (IOException ex) {
       throw new BeanInitializationException("Could not load properties", ex);
    }
}
protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
       StringValueResolver valueResolver) {

    BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);

    String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
    for (String curName : beanNames) {
       // Check that we're not parsing our own bean definition,
       // to avoid failing on unresolvable placeholders in properties file locations.
       if (!(curName.equals(this.beanName) & amp; & amp; beanFactoryToProcess.equals(this.beanFactory))) {
          BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
          try {
              //it's here
             visitor.visitBeanDefinition(bd);
          }
          catch (Exception ex) {
             throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
          }
       }
    }
}
public void visitBeanDefinition(BeanDefinition beanDefinition) {
    visitParentName(beanDefinition);
    visitBeanClassName(beanDefinition);
    visitFactoryBeanName(beanDefinition);
    visitFactoryMethodName(beanDefinition);
    visitScope(beanDefinition);
    if (beanDefinition.hasPropertyValues()) {
        //Here is to see if there is an attribute value in your beanDefinition
       visitPropertyValues(beanDefinition.getPropertyValues());
    }
    if (beanDefinition.hasConstructorArgumentValues()) {
       ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
       visitIndexedArgumentValues(cas.getIndexedArgumentValues());
       visitGenericArgumentValues(cas.getGenericArgumentValues());
    }
}

@Nullable
protected Object resolveValue(@Nullable Object value) {
    //The placeholder will be here
    else if (value instanceof TypedStringValue) {
       TypedStringValue typedStringValue = (TypedStringValue) value;
       String stringValue = typedStringValue.getValue();
       if (stringValue != null) {
          String visitedString = resolveStringValue(stringValue);
          typedStringValue.setValue(visitedString);
       }
    }
        return value;
}
@Nullable
protected String resolveStringValue(String strVal) {
    if (this.valueResolver == null) {
       throw new IllegalStateException("No StringValueResolver specified - pass a resolver " +
             "object into the constructor or override the 'resolveStringValue' method");
    }
    String resolvedValue = this.valueResolver.resolveStringValue(strVal);
    // Return original String if not modified.
    return (strVal.equals(resolvedValue) ? strVal : resolvedValue);
}
@Override
@Nullable
public String resolveStringValue(String strVal) throws BeansException {
    String resolved = this.helper.replacePlaceholders(strVal, this.resolver);
    if (trimValues) {
       resolved = resolved.trim();
    }
    return (resolved.equals(nullValue) ? null : resolved);
}
The next step is to follow the code bit by bit.
protected String parseStringValue(
       String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) {

    int startIndex = value.indexOf(this.placeholderPrefix);
    if (startIndex == -1) {
       return value;
    }

    StringBuilder result = new StringBuilder(value);
    while (startIndex != -1) {
       int endIndex = findPlaceholderEndIndex(result, startIndex);
       if (endIndex != -1) {
          String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
          String originalPlaceholder = placeholder;
          if (visitedPlaceholders == null) {
             visitedPlaceholders = new HashSet<>(4);
          }
          if (!visitedPlaceholders.add(originalPlaceholder)) {
             throw new IllegalArgumentException(
                   "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
          }
          // Recursive invocation, parsing placeholders contained in the placeholder key.
          placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
          // Now obtain the value for the fully resolved key...
          //Here we will take out our configuration in properties
          String propVal = placeholderResolver.resolvePlaceholder(placeholder);
          if (propVal == null & amp; & amp; this.valueSeparator != null) {
             int separatorIndex = placeholder.indexOf(this.valueSeparator);
             if (separatorIndex != -1) {
                String actualPlaceholder = placeholder.substring(0, separatorIndex);
                String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
                propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
                if (propVal == null) {
                   propVal = defaultValue;
                }
             }
          }
          //When our configuration is not empty, the next step is to replace the attribute placeholders in the beanDefinition.
          if (propVal != null) {
             // Recursive invocation, parsing placeholders contained in the
             // previously resolved placeholder value.
             propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
             result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
             if (logger.isTraceEnabled()) {
                logger.trace("Resolved placeholder '" + placeholder + "'");
             }
             startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
          }
          else if (this.ignoreUnresolvablePlaceholders) {
             // Proceed with unprocessed value.
             startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
          }
          else {
             throw new IllegalArgumentException("Could not resolve placeholder '" +
                   placeholder + "'" + " in value "" + value + """);
          }
          visitedPlaceholders.remove(originalPlaceholder);
       }
       else {
          startIndex = -1;
       }
    }
    return result.toString();
}

The above ignores many processes and there are too many codes, but the basic process is there. All the placeholders have been replaced above, so that when the corresponding bean is instantiated later, the value obtained in zk will be used.