How are external configuration property values bound to XxxProperties class properties?--SpringBoot Source

Note: This source analysis corresponds to SpringBoot version 2.1.0.RELEASE

1 Preface

This Join How does SpringBoot achieve automatic configuration?--SpringBoot Source (4)

So let's review the previous one briefly. In the previous one, we analyzed the related source code of SpringBoot auto-configuration. Auto-configuration related source code has the following important steps:

  1. Load the automatic configuration class from the spring.factories configuration file;

  2. Exclude the auto-configuration class specified by the exclude property of the @EnableAutoConfiguration annotation from the loaded auto-configuration class;

  3. Then use the AutoConfiguration ImportFilter interface to filter whether the auto-configuration class meets the criteria of its labels (if any)@ConditionalOnClass,@ConditionalOnBean and@ConditionalOnWebApplication and return the matching results if both are met;

  4. The AutoConfiguration ImportEvent event is then triggered, telling the ConditionEvaluationReport Conditional Assessment Reporter object to record the eligible and exclude d auto-configuration classes, respectively.

  5. Finally spring imports the last filtered auto-configuration class into the IOC container

Continuing with this article on the source code for SpringBoot's automatic configuration, let's examine the @EnableConfiguration Properties and @EnableConfiguration Properties annotations to explore how external configuration property values are bound to class properties in the @Configuration Properties annotation.

For example, if we want to configure the server port of a web project as 8081, we will configure server.port=8081 in the application.properties configuration file, where 8081 will be bound to the property port of the class ServerProperties annotated by @ConfigurationProperties to make the configuration effective.

2 @EnableConfigurationProperties

Let's go on to the chestnuts in the previous setup of the server port. Let's look directly at the source code of ServerProperties. We should be able to find the entry to the source code:

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
    /**
     * Server HTTP port.
     */
    private Integer port;
    // ...omit non-critical code
}

You can see that the @ConfigurationProperties annotation is marked on the ServerProperties class, that the server property configuration is prefixed with server, and that ignoreUnknownFields is set to true.

So let's look at the source code for the @ConfigurationProperties comment:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationProperties {

    // Prefix Alias
    @AliasFor("prefix")
    String value() default "";

    // prefix
    @AliasFor("value")
    String prefix() default "";

    // Ignore invalid configuration properties
    boolean ignoreInvalidFields() default false;

    // Ignore unknown configuration properties
    boolean ignoreUnknownFields() default true;
}

The purpose of the @ConfigurationProperties annotation is to bind the configuration values of an external configuration to the properties of its annotated class, which can act on the methods of the configuration class or the configuration class.You can see that the @ConfigurationProperties annotation unloads the set prefix, ignores properties such as nonexistent or invalid configurations, etc. There is no other processing logic for this annotation. You can see that @ConfigurationProperties is a symbolic annotation with no source entry here.

Here's the automatic configuration of the server. Naturally, let's look at the source code for the automatic configuration class ServletWebServerFactoryAutoConfiguration:

@Configuration
@EnableConfigurationProperties(ServerProperties.class)
// ...omit non-critical notes
public class ServletWebServerFactoryAutoConfiguration {
    // ...omit non-critical code
}

To highlight the point, I have omitted the non-critical code and non-critical comments for the ServletWebServerFactoryAutoConfiguration.As you can see, the ServletWebServerFactoryAutoConfiguration autoconfiguration class has a @EnableConfiguration Properties annotation, and the annotation value is the ServerProperties.class described earlier, so the @EnableConfiguration Properties annotation is certainly the focus of our attention.

Again, look at the source code for the @EnableConfigurationProperties annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {

    // The class specified by this value is the class labeled by the @ConfigurationProperties annotation and will be registered in the spring container
    Class<?>[] value() default {};

}

The main purpose of the @EnableConfigurationProperties annotation is to support classes labeled with the @ConfigurationProperties annotation by binding external configuration property values, such as application.properties configuration values, to the properties of classes labeled with @ConfigurationProperties.

Note: The Configuration PropertiesAutoConfiguration class also exists in the SpringBoot source code, and the EnableAutoConfiguration interface in the spring.factories configuration file also configures Configuration PropertiesAutoConfiguration, which also has a comment @EnableConfiguration Properties on it, with heap property binding turned on by default.

So, how does the @EnableConfiguration Properties annotation support property binding?

You can see that @EnableConfiguration PropertiesThis annotation also has @Import (EnableConfiguration PropertiesImportSelector.class), which imports EnableConfiguration PropertiesImportSelector, so you can be sure that the @EnableConfiguration PropertiesThis annotation's support for property binding must be related to EnableConfiguration PropertiesImportSelector.

Here, EnableConfigurationPropertiesImportSelector is the object that we will analyze next, so let's continue to analyze how EnableConfigurationPropertiesImportSelector assumes the responsibility of binding external configuration property values to the properties of the classes labeled with @ConfigurationProperties.

3 EnableConfigurationPropertiesImportSelector

The EnableConfigurationPropertiesImportSelector class is primarily used to handle logic related to external property binding. It implements the ImportSelector interface, and as we all know, the selectImports method that implements the ImportSelector interface registers bean s in containers.

So let's look at the selectImports method overridden by EnableConfigurationPropertiesImportSelector:

// EnableConfigurationPropertiesImportSelector.java

class EnableConfigurationPropertiesImportSelector implements ImportSelector {
        // The IMPORTS array is the bean to be registered with the spring container
    private static final String[] IMPORTS = {
            ConfigurationPropertiesBeanRegistrar.class.getName(),
            ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };

    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        // Returns the fully qualified name of ConfigurationPropertiesBeanRegistrar and ConfigurationPropertiesBindingPostProcessorRegistrar
        // That is, the above two classes will be registered in the Spring container
        return IMPORTS;
    }

}

You can see that the selectImports method in the EnableConfigurationPropertiesImportSelector class returns an array of IMPORTS, which is a constant array with values of ConfigurationPropertiesBeanRegistrar and ConfigurationPropertiesBindingPostProcessorRegistrar.That is, the EnableConfigurationPropertiesImportSelector registers ConfigurationPropertiesBeanRegistrar and ConfigurationPropertiesBindingPostProcessorRegistrar bean s in the Spring container.

We don't see any logic in the EnableConfigurationPropertiesImportSelector class to handle external property binding. Instead, we just registered two beans, ConfigurationPropertiesBeanRegistrar and ConfigurationPropertiesBindingPostProcessorRegistrar. Next, we'll look at the two beans registered.

4 ConfigurationPropertiesBeanRegistrar

Let's start with the ConfigurationPropertiesBeanRegistrar class.

ConfigurationPropertiesBeanRegistrar is an internal class of EnableConfigurationPropertiesImportSelector that implements the ImportBeanDefinitionRegistrar interface and overrides the registerBeanDefinitions method.Configuration PropertiesBeanRegistrar is used to register some bean definition s, that is, to register some beans in the Spring container.

First look at the source code for Configuration PropertiesBeanRegistrar:

// ConfigurationPropertiesBeanRegistrar$ConfigurationPropertiesBeanRegistrar.java

public static class ConfigurationPropertiesBeanRegistrar
            implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,  // Metadata is an AnnotationMetadataReadingVisitor object that stores metadata for a configuration class
            BeanDefinitionRegistry registry) {
        // (1) Get all the property values for the @EnableConfigurationProperties annotation,
        // For example, @EnableConfigurationProperties(ServerProperties.class), the resulting value is ServerProperties.class
        // (2) Then register all the property values of the resulting @EnableConfigurationProperties annotation in the container
        getTypes(metadata).forEach((type) -> register(registry,
                (ConfigurableListableBeanFactory) registry, type));
    }
}

In the registerBeanDefinitions implemented by ConfigurationPropertiesBeanRegistrar, you can see that there are two main things to do:

  1. Call the getTypes method to get the property value XxProperties for the @EnableConfigurationProperties annotation;
  2. Call the register method to register the obtained property value XxProperties into the Spring container for later use when Binding external properties.

Let's look at the source code for the getTypes method:

// ConfigurationPropertiesBeanRegistrar$ConfigurationPropertiesBeanRegistrar.java

private List<Class<?>> getTypes(AnnotationMetadata metadata) {
    // Gets all the property values for the @EnableConfiguration Properties annotation,
    // For example, @EnableConfigurationProperties(ServerProperties.class), the resulting value is ServerProperties.class
    MultiValueMap<String, Object> attributes = metadata
            .getAllAnnotationAttributes(
                    EnableConfigurationProperties.class.getName(), false);
    // Load property values out of List collection and return
    return collectClasses((attributes != null) ? attributes.get("value")
            : Collections.emptyList());
}

The logic in the getTypes method is simple: the property value XxProperties (such as ServerProperties.class) in the @EnableConfiguration Properties annotation is taken out, loaded into the List collection, and returned.

After getting the property value XxxProperties (such as ServerProperties.class) from the @EnableConfigurationProperties annotation by the getTypes method, iterate through registering the XxxProperties in the Spring container one by one. Let's look at the register method:

// ConfigurationPropertiesBeanRegistrar$ConfigurationPropertiesBeanRegistrar.java

private void register(BeanDefinitionRegistry registry,
        ConfigurableListableBeanFactory beanFactory, Class<?> type) {
    // Get the name of the type, typically using the fully qualified name of the class as the bean name
    String name = getName(type);
    // Determine if the bean is included in the beanFactory container based on the bean name
    if (!containsBeanDefinition(beanFactory, name)) {
        // If not, register the bean definition
        registerBeanDefinition(registry, name, type);
    }
}

Let's look at another class, ConfigurationPropertiesBindingPostProcessorRegistrar, imported by EnableConfigurationPropertiesImportSelector.

5 ConfigurationPropertiesBindingPostProcessorRegistrar

You can see that the ConfigurationPropertiesBindingPostProcessorRegistrar class name ends with the Registrar word again, indicating that it must have imported some bean definition s again.Look directly at the source code:

// ConfigurationPropertiesBindingPostProcessorRegistrar.java

public class ConfigurationPropertiesBindingPostProcessorRegistrar
        implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
            BeanDefinitionRegistry registry) {
        // If Configuration PropertiesBindingPostProcessor is not registered in the container as a postprocessor to handle property binding,
        // Then Configuration PropertiesBindingPostProcessor and Configuration BeanFactoryMetadata bean s will be registered
        // Note that the onApplicationEnvironmentPreparedEvent event loads configuration properties first, then registers some post-processors to handle them
        if (!registry.containsBeanDefinition(
                ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) {
            // (1) Register Configuration PropertiesBindingPostProcessor Post Processor to post-process configuration properties
            registerConfigurationPropertiesBindingPostProcessor(registry);
            // (2) Register a ConfigurationBeanFactoryMetadata type bean,
            // Note that ConfigurationBeanFactoryMetadata implements BeanFactoryPostProcessor, which then registers some metadata in postProcessBeanFactory
            registerConfigurationBeanFactoryMetadata(registry);
        }
    }
    // Register Configuration PropertiesBindingPostProcessor Post Processor
    private void registerConfigurationPropertiesBindingPostProcessor(
            BeanDefinitionRegistry registry) {
        GenericBeanDefinition definition = new GenericBeanDefinition();
        definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class);
        definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        registry.registerBeanDefinition(
                ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition);

    }
    // Register ConfigurationBeanFactoryMetadata Post Processor
    private void registerConfigurationBeanFactoryMetadata(
            BeanDefinitionRegistry registry) {
        GenericBeanDefinition definition = new GenericBeanDefinition();
        definition.setBeanClass(ConfigurationBeanFactoryMetadata.class);
        definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME,
                definition);
    }

}

The logic of the ConfigurationPropertiesBindingPostProcessorRegistrar class is very simple, and it is mainly used to register external configuration property binding-related post processors, ConfigurationBeanFactoryMetadata and ConfigurationPropertiesBindingPostProcessor.

Then let's explore what kind of postprocessing logic these two registered postprocessors perform.

6 ConfigurationBeanFactoryMetadata

Let's start with ConfigurationBeanFactoryMetadata, a postProcessBeanFactory post processor that implements the BeanFactoryPostProcessor interface's postProcessBeanFactory method to store the @Bean annotation's metadata when the bean factory is initialized for use in subsequent logic related to configuring attribute bindings externally.

Let's first look at the postProcessBeanFactory method source code for the ConfigurationBeanFactoryMetadata class that implements the BeanFactoryPostProcessor interface:

// ConfigurationBeanFactoryMetadata

public class ConfigurationBeanFactoryMetadata implements BeanFactoryPostProcessor {

    /**
     * The bean name that this class is registered with.
     */
    public static final String BEAN_NAME = ConfigurationBeanFactoryMetadata.class
            .getName();

    private ConfigurableListableBeanFactory beanFactory;
    /**
     * beansFactoryMetadata Collection stores metadata for beansFactory
     * key:The name of a bean value:FactoryMetadata object (encapsulates the factory bean name and the factory method name)
     * For example, the following configuration class:
     *
     * @Configuration
     * public class ConfigA {
     *      @Bean
     *      public BeanXXX methodB(configA, ) {
     *          return new BeanXXX();
     *      }
     * }
     *
     * Then: key value is "methodB", value is a FactoryMetadata (configA, methodB) object, its bean attribute value is "configA",method attribute value is "methodB"
     */
    private final Map<String, FactoryMetadata> beansFactoryMetadata = new HashMap<>();

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
            throws BeansException {
        this.beanFactory = beanFactory;
        // beanDefinitionName that traverses the beanFactory, that is, the name of each bean (such as the name of the bean corresponding to the factory method)
        for (String name : beanFactory.getBeanDefinitionNames()) {
            // Get beanDefinition from name
            BeanDefinition definition = beanFactory.getBeanDefinition(name);
            // Factory method name: typically a method name with a comment @Bean
            String method = definition.getFactoryMethodName();
            // Factory bean name: typically a class name with the comment @Configuration
            String bean = definition.getFactoryBeanName();
            if (method != null && bean != null) {
                // Load the FactoryMetadata object that encapsulates the factory bean name and the factory method name into the beansFactoryMetadata as a value using the beanDefinitionName as the Key
                this.beansFactoryMetadata.put(name, new FactoryMetadata(bean, method));
            }
        }
    }
}

From the code above, you can see that the postProcessBeanFactory method overridden by the ConfigurationBeanFactoryMetadata class does the same thing as caching some metadata of the factory Bean (a class that can be understood as the @Configuration annotation) and its @Bean annotation's factory method into the beansFactoryMetadata collection for subsequent use, which is described later.

As we can see from the code above that the beansFactoryMetadata collection type of the ConfigurationBeanFactoryMetadata class is Map< String, FactoryMetadata>, let's look again at the FactoryMetadata class encapsulating the relevant factory metadata:

// ConfigurationBeanFactoryMetadata$FactoryMetadata.java

private static class FactoryMetadata {
    // Class name of the configuration class for the @Configuration annotation
    private final String bean;
    // Method name for @Bean annotation
    private final String method;

    FactoryMetadata(String bean, String method) {
        this.bean = bean;
        this.method = method;
    }

    public String getBean() {
        return this.bean;
    }

    public String getMethod() {
        return this.method;
    }

}

FactoryMetadata has only two attribute beans and methods that represent the factory beans for the @Configuration annotation and the factory methods for the @Bean annotation, respectively.

So much said above, it would be more intuitive to hold a chestnut directly:

/**
 * beansFactoryMetadata Collection stores metadata for beansFactory
 * key:The name of a bean value:FactoryMetadata object (encapsulates the factory bean name and the factory method name)
 * For example, the following configuration class:
 *
 * @Configuration
 * public class ConfigA {
 *      @Bean
 *      public BeanXXX methodB(configA, ) {
 *          return new BeanXXX();
 *      }
 * }
 *
 * Then: key value is "methodB", value is a FactoryMetadata (configA, methodB) object, its bean attribute value is "configA",method attribute value is "methodB"
 */
 private final Map<String, FactoryMetadata> beansFactoryMetadata = new HashMap<>();

To better understand what the above beansFactoryMetadata collection stores, it is recommended that you debug it yourself to see what's inside.All in all, it's good to remember that the beansFactoryMetadata collection of the ConfigurationBeanFactoryMetadata class stores metadata about the factory beans for use in the ConfigurationPropertiesBindingPostProcessor post-processor.

7 ConfigurationPropertiesBindingPostProcessor

Let's look at ConfigurationPropertiesBindingPostProcessorRegistrar, another postprocessor registered with the ConfigurationPropertiesBindingPostProcessor class, which is particularly important for binding external configuration properties to the properties of the XxxProperties class labeled with the @ConfigurationProperties annotation (for example, the application.properties configuration text).If server.port=8081 is set in the file, then 8081 will be bound to the implementation logic of the Port property of the ServerProperties class.

Similarly, first look at the source code for Configuration PropertiesBindingPostProcessor:

// ConfigurationPropertiesBindingPostProcessor.java

public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
    PriorityOrdered, ApplicationContextAware, InitializingBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        // ... Omit the implementation code here first
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        // ... Omit the implementation code here first
    }

    // ...omit non-critical code
}

You can see that the Configuration PropertiesBindingPostProcessor postprocessor implements two important interfaces, InitializingBean and BeanPostProcessor.

We all know:

  1. The afterPropertiesSet method of the InitializingBean interface is called after the bean attribute has been assigned to it to perform some custom initialization logic, such as checking whether certain mandatory attributes are assigned, checking certain configurations, or assigning values to some unassigned attributes.
  2. The BeanPostProcessor interface is the bean's postprocessor. It has two hook methods, postProcessBeforeInitialization and postProcessAfterInitialization, which are called before and after the bean's initialization to perform some postprocessing logic, such as checking the tag interface or wrapping the bean with a proxy.

You can also see from the code above that the Configuration PropertiesBindingPostProcessor postprocessor overrides the After PropertiesSet method of InitializingBean and the postProcessBeforeInitialization method of BeanPostProcessor.

Next, let's explore the source code for the two methods that Configuration PropertiesBindingPostProcessor overrides.

7.1 Prepare related metadata and configure property binders before executing external property binding logic

Let's first analyze the ConfigurationPropertiesBindingPostProcessor override the afterPropertiesSet method of the InitializingBean interface:

// ConfigurationPropertiesBindingPostProcessor.java

        /**
     * Configure Attribute Checker Name
     */
    public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";
    /**
     * Factory bean related metadata
     */
    private ConfigurationBeanFactoryMetadata beanFactoryMetadata;
    /**
     * context
     */
    private ApplicationContext applicationContext;
    /**
     * Configure Property Binder
     */
    private ConfigurationPropertiesBinder configurationPropertiesBinder;

    // The main purpose here is to assign values to the properties of beanFactoryMetadata and configurationPropertiesBinder, which are used later in the post-processor method to handle property binding
    @Override
    public void afterPropertiesSet() throws Exception {
        // We can't use constructor injection of the application context because
        // it causes eager factory bean initialization
        // [1] Use the afterPropertiesSet hook method to get the previously registered ConfigurationBeanFactoryMetadata object from the container and assign it to the beanFactoryMetadata property
        // (Question 1) When was the bean FactoryMetadata registered in the container?
        // (Answer 1) Register the bean FactoryMetadata in the registerBeanDefinitions method of the Configuration PropertiesBindingPostProcessorRegistrar class into the container
        // (Question 2) When will the beanFactoryMetadata object be used when it is retrieved from the container?
        // (Answer 2) The beansFactoryMetadata collection of the beanFactoryMetadata object holds the factory bean-related metadata in the ConfigurationPropertiesBindingPostProcessor class
        //        To determine whether a bean has a FactoryAnnotation or a FactoryMethod, it is searched based on the metadata of the beansFactoryMetadata collection of the beanFactoryMetadata object
        this.beanFactoryMetadata = this.applicationContext.getBean(
                ConfigurationBeanFactoryMetadata.BEAN_NAME,
                ConfigurationBeanFactoryMetadata.class);
        // [2] new Configuration PropertiesBinder, used for subsequent external property bindings
        this.configurationPropertiesBinder = new ConfigurationPropertiesBinder(
                this.applicationContext, VALIDATOR_BEAN_NAME); // VALIDATOR_BEAN_NAME="configurationPropertiesValidator"
    }

You can see that the main logic of the code above is to prepare the relevant metadata and configure the property binder before executing the external property binding logic, that is, to get the beanFactoryMetadata property from the Spring container that was previously registered as the Configuration BeanFactoryMetadata object and assign it to the Configuration PropertiesBindingPostProcessor postprocessor.Another option is to create a new Configuration PropertiesBinder configuration property binder object and assign it to the Configuration PropertiesBinder property.

Let's look again at how the Configuration PropertiesBinder configuration property binder object is constructed.

// ConfigurationPropertiesBinder.java

ConfigurationPropertiesBinder(ApplicationContext applicationContext,
        String validatorBeanName) {
    this.applicationContext = applicationContext;
    // Encapsulate the applicationContext into the PropertySourcesDeducer object and return
    this.propertySources = new PropertySourcesDeducer(applicationContext)
            .getPropertySources(); // Gets the property source, primarily used in the postprocessing method postProcessBeanFactory of ConfigurableListableBeanFactory
    // If validator is not configured, null is generally returned here
    this.configurationPropertiesValidator = getConfigurationPropertiesValidator(
            applicationContext, validatorBeanName);
    // Check the existence of a bean validator-related class implementing the JSR-303 specification in the classpath
    this.jsr303Present = ConfigurationPropertiesJsr303Validator
            .isJsr303Present(applicationContext);
}

You can see that the ConfigurationPropertiesBinder object is constructed mainly by assigning values to its related properties (as is the case with general constructor logic):

  1. Inject a context object to assign a value to the applicationContext property;
  2. Assign a value to the propertySources property, which is the property source of an external configuration value such as application.properties configuration. Note that the property source here is read by the ConfigFileApplicationListener, which is detailed in the Source Code Analysis section later.
  3. Assign a value to the configurationPropertiesValidator property from a bean named configurationPropertiesValidator in the Spring container.
  4. Assigning a value to the jsr303Present attribute is true when the values of the jsr303Present attribute exist in the classpath at the same time as javax.validation.Validation.ValidatorFactory and javax.validation.bootstrap.GenericBootstrap.

About JSR303:JSR-303 is a subspecification of JAVA EE 6 called Bean Validation, and Hibernate Validator is a reference implementation of Bean Validation.Hibernate Validator provides implementations of all built-in constraints in the JSR 303 specification, with additional constraints.

7.2 Execute true external attribute binding logic [Main Line]

In the previous analysis, we found that we have not yet reached the real processing logic of external attribute binding. The previous steps are to do some preparatory work to pave the way for external attribute binding.

Now that you have the relevant metadata and configuration property binders ready before executing the external property binding logic, let's take a look at the postProcessBeforeInitialization post-processing method that ConfigurationPropertiesBindingPostProcessor implements for the BeanPostProcessor interface. External property binding logic is implemented in this post-processing method, which is the focus of our attention..

Look directly at the code:

// ConfigurationPropertiesBindingPostProcessor.java

// Because it's an external configuration property postprocessor, post-process the XxxProperties class labeled by the @ConfigurationProperties annotation to complete the property binding here
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
        throws BeansException {
    // Note that the BeanPostProcessor post-processor handles all beans by default, so you need to filter based on some conditions of the beans to get the final destination beans to process.
    // The filter here is to determine if a bean has the @ConfigurationProperties annotation
    // [1] Get the @ConfigurationProperties annotation from the bean and return it if the bean has a label; if it does not, return Null.For example, the @ConfigurationProperties annotation is marked on ServerProperty
    ConfigurationProperties annotation = getAnnotation(bean, beanName,
            ConfigurationProperties.class);
    // [2] If a bean is labeled with the @ConfigurationProperties annotation, it is further processed by injecting the configuration of the configuration file into the attribute value of the bean
    if (annotation != null) {
        /********Main Line, Focus on ******/
        bind(bean, beanName, annotation); 
    }
    // [3] Returns bean s bound by external configuration property values (typically XxProperties objects)
    return bean;
}

What the postProcessBeforeInitialization method overridden by the ConfigurationPropertiesBindingPostProcessor class does is bind the external property configuration to the XxxProperties class labeled by the @ConfigurationProperties annotation. The key steps are summarized below:

  1. Get the @ConfigurationProperties annotation from the bean;
  2. If a bean is labeled with the @ConfigurationProperties annotation, further processing is performed: the beans are returned after the external configuration property values are bound to the beans'property values; if no beans are labeled with the @ConfigurationProperties annotation, the beans are returned directly as is.

Note: Postprocessors post-process beans in each container by default because only beans labeled with the @ConfigurationProperties annotation are bound externally, so beans without the @ConfigurationProperties annotation will not be processed.

Next, let's follow the main line and see how the external configuration properties are bound to the XxxProperties class properties of the @ConfigurationProperties annotation.

Look directly at the code:

// ConfigurationPropertiesBindingPostProcessor.java

private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
    // [1] Get the type of bean, such as ServerPropertie The type of bean is: org.springframework.boot.autoconfigure.web.ServerProperties
    ResolvableType type = getBeanType(bean, beanName);
    // [2] Get the @Validated annotation labeled on the bean
    Validated validated = getAnnotation(bean, beanName, Validated.class);
    // If the @Validated annotation is marked, an Annotation array is formed with the @ConfigurationProperties annotation
    Annotation[] annotations = (validated != null)
            ? new Annotation[] { annotation, validated }
            : new Annotation[] { annotation };
    // [3] Returns a Bindable object target bound to the XxxProperties class, which is the target object injected by external property values
    // (such as a Bindable object encapsulating a ServerProperties object labeled with the @ConfigurationProperties annotation)
    Bindable<?> target = Bindable.of(type).withExistingValue(bean)
            .withAnnotations(annotations); // Set annotations property array
    try {
        // [4] Execute external configuration property binding logic
        /********[Main Line, Focus on ******/
        this.configurationPropertiesBinder.bind(target);
    }
    catch (Exception ex) {
        throw new ConfigurationPropertiesBindException(beanName, bean, annotation,
                ex);
    }
}

Key Step The code above has been labeled [x], and here we continue with the main line logic for external configuration property binding (in the section <font color=blue>8 Configuration PropertiesBinder</font>) Interpolate a point of knowledge first, remember that the postProcessBeanFactory method overridden by ConfigurationBeanFactoryMetadata already encapsulates metadata of the related factory beans into the beansFactoryMetadata collection of the ConfigurationBeanFactoryMetadata class?

Let's look again at the [1] getBeanType and [2] getAnnotation method sources in the code above:

// ConfigurationPropertiesBindingPostProcessor.java

private ResolvableType getBeanType(Object bean, String beanName) {
    // First get if there is a factory method
    Method factoryMethod = this.beanFactoryMetadata.findFactoryMethod(beanName);
    // If there is a factory method
    if (factoryMethod != null) { 
        return ResolvableType.forMethodReturnType(factoryMethod);
    } 
    // No factory method means a generic configuration class
    return ResolvableType.forClass(bean.getClass());
}

private <A extends Annotation> A getAnnotation(Object bean, String beanName,
        Class<A> type) {
    A annotation = this.beanFactoryMetadata.findFactoryAnnotation(beanName, type);
    if (annotation == null) {
        annotation = AnnotationUtils.findAnnotation(bean.getClass(), type);
    }
    return annotation;
}

Notice that the beanFactoryMetadata object in the code above is missing, and the getBeanType and getAnnotation methods of the Configuration PropertiesBindingPostProcessor post-processor call the findFactoryMethod and findFactoryAnnotation methods of Configuration BeanFactoryMetadata, respectively, while the findFactoryMethod and findFactoryAnnotation methods of Configuration BeanFactoryMetadata depend on themA collection of beansFactoryMetadata that stores factory bean metadata to find out if there are FactoryMethods and FactoryAnnotation.So here we know that the beansFactoryMetadata collection of ConfigurationBeanFactoryMetadata stores factory bean metadata.

8 ConfigurationPropertiesBinder

Let's continue to follow the main line of external configuration property binding and look ahead to <font color=blue>7.2 to execute this.configurationPropertiesBinder.bind(target) in the true external property binding logic </font>

// ConfigurationPropertiesBinder.java

public void bind(Bindable<?> target) {
    //[1] Get the @ConfigurationProperties annotation
    ConfigurationProperties annotation = target
            .getAnnotation(ConfigurationProperties.class);
    Assert.state(annotation != null,
            () -> "Missing @ConfigurationProperties on " + target);
    // [2] Get a set of Validator objects for attribute checking
    List<Validator> validators = getValidators(target);
    // [3] Get the BindHandler object (default is IgnoreTopLevelConverterNotFoundBindHandler object),
    // Handling attributes such as ignoreUnknownFields for the Configuration Properties annotation
    BindHandler bindHandler = getBindHandler(annotation, validators);
    // [4] Get a Binder object and use its bind method to execute external property binding logic
    /********************[Main Line, Focus on ******************/
    getBinder().bind(annotation.prefix(), target, bindHandler);
}

The main logic of the code above is:

  1. Get the @ConfigurationProperties annotation and validator (if any) on the target object (corresponding to the XxxProperties class);
  2. The BindHandler object is then obtained from the @ConfigurationProperties annotation and validator obtained, which is used to process some attachment logic when Binding attributes; analysis </font> in section <font color=blue>8.1.
  3. Finally, get a Binder object and call its bind method to execute the logic of external property binding. Analyze </font> in section <font color=blue>8.2.

8.1 Get a BindHandler object to handle some attachment logic when binding properties

Let's get to know what BindHandler does before we look at the logic of the getBindHandler method.

BindHandler is a parent interface for handling some attachment logic when Binding properties.Let's first look at the class diagram of BindHandler to get a general idea:


<center>Figure 1</center>

You can see that AbstractBindHandler implements the BindHandler interface as an abstract base class with four specific subclasses, IgnoreTopLevelConverterNotFoundBindHandler,NoUnboundElementsBindHandler,IgnoreErrorsBindHandler, and ValidationBindHandler.

  1. IgnoreTopLevelConverterNotFoundBindHandler: The default BindHandler when handling external property binding, the top-level ConverterNotFoundException is ignored when property binding fails;
  2. NoUnboundElementsBindHandler: Unknown property used to handle profile configuration;
  3. IgnoreErrorsBindHandler: Used to ignore invalid configuration properties such as type errors;
  4. ValidationBindHandler: A validator is used to validate the result value of a binding.

After analyzing the class relationships, let's look again at what methods the BindHandler interface provides to provide additional attachment logic when Binding external properties, looking directly at the code:

// BindHandler.java

public interface BindHandler {

    /**
     * Default no-op bind handler.
     */
    BindHandler DEFAULT = new BindHandler() {

    };

    // onStart method is called before external property binding
    default <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
            BindContext context) {
        return target;
    }

    // The onSuccess method is called when an external property is successfully bound, which can change the final returned property value or verify it
    default Object onSuccess(ConfigurationPropertyName name, Bindable<?> target,
            BindContext context, Object result) {
        return result;
    }

    // The onFailure method is called when an external property binding fails, including a logical execution failure in the onSuccess method.
    // This method can be used to catch related exceptions or return an alternative result (a little like the downgrade of a microservice, hey-hey)
    default Object onFailure(ConfigurationPropertyName name, Bindable<?> target,
            BindContext context, Exception error) throws Exception {
        throw error;
    }

    // Called when the external property binding ends, whether it succeeds or fails
    default void onFinish(ConfigurationPropertyName name, Bindable<?> target,
            BindContext context, Object result) throws Exception {
    }
}

You can see that the BindHandler interface defines onStart, onSuccess,The onFailure and onFinish methods, which are called at different times when an external property binding is performed, are used to add additional processing logic when the property binding is performed, such as changing or checking the property value of the final binding in the onSuccess method, catch ing related exceptions in the onFailure method, or returning an alternative binding property value.

Knowing that BindHandler adds some additional attachment handling logic when Binding attributes, let's look at the logic of the getBindHandler method and go directly to the code:

// ConfigurationPropertiesBinder.java

// Note that BindHandler's design skills should be a chain of responsibility model, which is very clever and can be used for reference
private BindHandler getBindHandler(ConfigurationProperties annotation,
        List<Validator> validators) {
    // Create a new IgnoreTopLevelConverterNotFoundBindHandler object, which is the default BindHandler object
    BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler();
    // If the ignoreInvalidFields property of the comment @ConfigurationProperties is set to true,
    // A new IgnoreErrorsBindHandler object is created when invalid configuration properties such as type errors can be ignored
    if (annotation.ignoreInvalidFields()) {
        handler = new IgnoreErrorsBindHandler(handler);
    }
    // If the ignoreUnknownFields property of the comment @ConfigurationProperties is set to true,
    // Then the configuration file has some unknown property configurations configured, and a new ignoreUnknownFields object is created
    if (!annotation.ignoreUnknownFields()) {
        UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();
        handler = new NoUnboundElementsBindHandler(handler, filter);
    }
    // Create a ValidationBindHandler object if the @Valid comment is not empty
    if (!validators.isEmpty()) {
        handler = new ValidationBindHandler(handler,
                validators.toArray(new Validator[0]));
    }
    // Traversing through the acquired Configuration PropertiesBindHandlerAdvisor collection,
    // Configuration PropertiesBindHandlerAdvisor is currently only used in test classes
    for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
        // Further processing of handler
        handler = advisor.apply(handler);
    }
    // Return handler
    return handler;
}

The logic of the getBindHandler method is simple, primarily by creating different BindHandler implementation classes based on the @ConfigurationProperties annotation and validators passed in:

  1. First, a new IgnoreTopLevelConverterNotFoundBindHandler is used as the default BindHandler;
  2. If the property ignoreInvalidFields value of the @ConfigurationProperties annotation is true, then a new IgnoreErrorsBindHandler object passes in the newly created IgnoreTopLevelConverterNotFoundBindHandler object as a construction parameter to the parent property of the AbstractBindHandler parent class;
  3. If the ignoreUnknownFields value of the @ConfigurationProperties annotation is false, a new UnboundElementsSourceFilter object is passed in as a construction parameter to the parent property of the Abstract BindHandler parent class.
  4. ... and so on, using the parent property of the AbstractBindHandler parent class as the construction parameter for the latter hangdler object, each handler is chained together and the final handler is constructed.

GET Tip: Is this design pattern familiar? This is the responsibility chain pattern.We learn the source code as well as how other people are proficient in using design patterns.There are many application cases of responsibility chain mode, such as the various filters of Dubbo (such as AccessLogFilter to log access to services, ExceptionFilter to handle exceptions...), the Filter of Servlet when we started learning java web, the Plugin s of MyBatis, and the Pipline of Netty all adopt responsibility chain mode.

Now that we know what BindHandler does, follow the main line to see how attribute bindings are bound.

8.2 Get the Binder object for attribute binding [Main Line]

Here, follow the main line code getBinder().bind(annotation.prefix(), target, bindHandler) labeled [4] in the <font color=blue>8 Configuration PropertiesBinder</font>section code;

You can see that this code does two main things:

  1. Call the getBinder method to get the Binder object for property binding;
  2. Call the Binder object's bind method to bind external properties to the properties of the XxxProperties class of the @ConfigurationProperties annotation.

So let's first look at the getBinder method source code:

// ConfigurationPropertiesBinder.java

private Binder getBinder() {
    // Binder is a container object that binds ConfigurationPropertySource
    if (this.binder == null) {
        // Create a new Binder object that encapsulates ConfigurationPropertySources.
        // PropertySourcesPlaceholders Resolver, ConversionService, and ProertyEditorInitializer objects
        this.binder = new Binder(getConfigurationPropertySources(), // Encapsulate the PropertySources object as a SpringConfiguration PropertySources object and return
                getPropertySourcesPlaceholdersResolver(), getConversionService(), // Encapsulate the PropertySources object as a PropertySourcesPlaceholdersResolver object and return it to get the ConversionService object from the container
                getPropertyEditorInitializer()); // Get the Consumer<PropertyEditorRegistry>object, which is used to configure property editors, which are often used to convert values
    }
    // Return to binder
    return this.binder;
}

You can see that the Binder object encapsulates four objects, ConfigurationPropertySources, PropertySourcesPlaceholders Resolver, ConversionService, and ProertyEditorInitializer. The Binder object encapsulates these four objects, which are certainly used in the later property binding logic. Let's first look at what these four objects do:

  • ConfigurationPropertySources: The property source of an external configuration file whose read is triggered by the ConfigFileApplicationListener listener;
  • PropertySourcesPlaceholders Resolver: Resolve placeholder ${} in property source;
  • ConversionService: Convert attribute types
  • PropertyEditorInitializer: Used to configure property editors

Now that we have the Binder property binder, let's see how its bind method performs property binding.

// Binder.java

public <T> BindResult<T> bind(String name, Bindable<T> target, BindHandler handler) {
    // ConfigurationPropertyName.of(name): Encapsulates name (in this case, property prefix name) into a ConfigurationPropertyName object
    // Bind external configuration properties to target object target
    return bind(ConfigurationPropertyName.of(name), target, handler); 
}

public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> target,
        BindHandler handler) {
    Assert.notNull(name, "Name must not be null");
    Assert.notNull(target, "Target must not be null");
    handler = (handler != null) ? handler : BindHandler.DEFAULT;
    // Context is an internal class of Binder that implements BindContext, which can be understood as the context of Binder and used to obtain properties of binder such as the source property of Binder
    Context context = new Context();
    // Make a property binding and return the bound of the object after the property is bound. Note that the bound object type is T, and T is the class of the @ConfigurationProperties annotation, such as ServerProperties
    /********[Main Line, Focus on ********/
    T bound = bind(name, target, handler, context, false);
    // Encapsulate the bound object just returned into the BindResult object and return
    return BindResult.of(bound);
}

The code above first creates a Context object, which is an internal class of Binder. It is the context of Binder. Using the Context context, you can get Binder's properties such as the source property value of Binder and bind it to the XxxProperties property.Then let's take a closer look at the bind(name, target, handler, context, false) method source line:

// Binder.java

protected final <T> T bind(ConfigurationPropertyName name, Bindable<T> target,
        BindHandler handler, Context context, boolean allowRecursiveBinding) {
    // Empty Binder's configurationProperty property value
    context.clearConfigurationProperty(); 
    try { 
        // [1] Call BindHandler's onStart method to execute a series of responsibility chain objects
        target = handler.onStart(name, target, context);
        if (target == null) {
            return null;
        }// [2] Call the bindObject method to bind the value of the external configuration to the properties of the Bindable object target and return the value assigned to the bound object.
        // Take a chestnut: If server.port=8888 is set, for example, then the method will eventually call the Binder.bindProperty method and return a bound with a value of 8888
        /************[Main line: Focus on *********/
        Object bound = bindObject(name, target, handler, context, 
                allowRecursiveBinding);
        // [3] Encapsulate the handleBindResult object and return it. Note that the onSucess, onFinish method of BindHandler is called in the constructor of handleBindResult
        return handleBindResult(name, target, handler, context, bound); 
    }
    catch (Exception ex) {
        return handleBindError(name, target, handler, context, ex);
    }
}

The comments for the code above are already very detailed and are not detailed here.Let's follow the main line to see the bindObject method source code:

// Binder.java

private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target,
        BindHandler handler, Context context, boolean allowRecursiveBinding) {
    // From the configuration properties in propertySource, get the ConfigurationProperty object property if there is a configuration in the application.properties configuration file.
    // Then property will not be null.Lift a chestnut: If you configure spring.profiles.active=dev in the configuration file, the corresponding property value is dev; otherwise, null
    ConfigurationProperty property = findProperty(name, context);
    // If property is null, no subsequent property binding related logic will be performed
    if (property == null && containsNoDescendantOf(context.getSources(), name)) {
        // Returns null if property == null
        return null;
    }
    // Get different Binders based on the target type, which can be null (the common type is generally null), MapBinder,CollectionBinder, or ArrayBinder
    AggregateBinder<?> aggregateBinder = getAggregateBinder(target, context);
    // If aggregateBinder does not configure null, for example, the spring.profiles attribute (including, of course, its sub-attributes, such as spring.profiles.active, etc.)
    if (aggregateBinder != null) {
        // If aggregateBinder is not null, call bindAggregate and return the bound object
        return bindAggregate(name, target, handler, context, aggregateBinder);
    }
    // If property is not null
    if (property != null) {
        try {
            // Binding properties to objects, such as server.port=8888 set in the configuration file, will ultimately call the bindProperty method for property setting
            return bindProperty(target, context, property);
        }
        catch (ConverterNotFoundException ex) {
            // We might still be able to bind it as a bean
            Object bean = bindBean(name, target, handler, context,
                    allowRecursiveBinding);
            if (bean != null) {
                return bean;
            }
            throw ex;
        }
    }
    // Only classes annotated by @ConfigurationProperties will walk here with external property binding
    /***********************[Main Line, Focus on ****************************/
    return bindBean(name, target, handler, context, allowRecursiveBinding);
}

From the code above, you can see that the logic to perform attribute binding in bindObject goes into different binding logic according to different attribute types, raising a chestnut:

  1. If spring.profiles.active=dev is configured in the application.properties configuration file, it will enter the return bindAggregate(name, target, handler, context, aggregateBinder); the code logic for this property binding;
  2. If server.port=8081 is configured in the application.properties configuration file, then the logic for property binding of return bindBean(name, target, handler, context, allowRecursiveBinding); will be entered.

So again, we follow the main line into the bindBean method in the attribute binding logic of the XxxProperties class of the @ConfigurationProperties annotation:

// Binder.java

private Object bindBean(ConfigurationPropertyName name, Bindable<?> target, // Name refers to the prefix name of Configuration Properties
        BindHandler handler, Context context, boolean allowRecursiveBinding) {
    // Here are some checks for ConfigurationPropertyState
    if (containsNoDescendantOf(context.getSources(), name)
            || isUnbindableBean(name, target, context)) {
        return null;
    }// A new implementation class object for BeanPropertyBinder is created here. Note that this object implements the bindProperty method
    BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(
            name.append(propertyName), propertyTarget, handler, context, false);
    /**
     * (propertyName, propertyTarget) -> bind(
     *              name.append(propertyName), propertyTarget, handler, context, false);
     *  Equivalent to
     *  new BeanPropertyBinder() {
     *      Object bindProperty(String propertyName, Bindable<?> target){
     *          bind(name.append(propertyName), propertyTarget, handler, context, false);
     *      }
     *  }
     */
    // Typee is the XxProperties class labeled with the @ConfigurationProperties annotation
    Class<?> type = target.getType().resolve(Object.class);
    if (!allowRecursiveBinding && context.hasBoundBean(type)) {
        return null;
    }
    // The lambda grammar of java8 is applied here. As a lambda grammar of java8 I don't understand the logic below very well. Ha-ha
    // This is the lambda code that really implements the logic of binding external configuration properties to the properties of the XxxProperties class in the @ConfigurationProperties annotation
    /*******************[Main Line ***************************************/
    return context.withBean(type, () -> {
        Stream<?> boundBeans = BEAN_BINDERS.stream()
                .map((b) -> b.bind(name, target, context, propertyBinder));
        return boundBeans.filter(Objects::nonNull).findFirst().orElse(null);
    });
    // Translate the following from the lambda statement above:
    /** Here T refers to various property-bound objects, such as ServerProperties
     * return context.withBean(type, new Supplier<T>() {
     *  T get() {
     *      Stream<?> boundBeans = BEAN_BINDERS.stream()
     *                  .map((b) -> b.bind(name, target, context, propertyBinder));
     *          return boundBeans.filter(Objects::nonNull).findFirst().orElse(null);
     *        }
     *  });
     */
}

From the code above, we have come to the lower level code where the external configuration properties are bound to the XxxProperties class properties, and you can see that the logic of the property binding should be in the lambda code labeled Main Line above.This is no longer detailed here, as it falls within the category of SpringBoot's attribute binding Binder, where Binder-related classes only appeared in SpringBoot 2.0, overriding previous attribute binding-related codes.Attribute binding is also related to many sources, there is a need to open another article to analyze and explore.

9 Summary

Well, that's the end of the source code analysis on how external configuration property values are bound to XxxProperties class properties. It's a long article and I don't know what I'm saying. Here's a summary of the important steps:

  1. First, the @EnableConfigurationProperties annotation import s the EnableConfigurationPropertiesImportSelector postprocessor;
  2. The EnableConfiguration PropertiesImportSelector postprocessor registers Configuration PropertiesBeanRegistrar and Configuration PropertiesBindingPostProcessorRegistrar bean s with the Spring container.
  3. ConfigurationPropertiesBeanRegistrar registers bean s of type XxxProperties with the Spring container; ConfigurationPropertiesBindingPostProcessorRegistrar registers ConfigurationBeanFactoryMetadata and ConfigurationPropertiesBindingPostProcessor with the Spring container.
  4. The ConfigurationBeanFactoryMetadata postprocessor stores the @Bean annotation's metadata when initializing the bean factory for use in subsequent external configuration property binding related logic;
  5. The Configuration PropertiesBindingPostProcessor postprocessor delegates the logic of binding external configuration property values to XxxProperties class properties to the Configuration PropertiesBinder object, which in turn delegates the logic of property binding to the Binder object.

Visible, what's important is step 5 above.

Originality is not easy. Give me a compliment!

PS: This article was meant to start analyzing the SpringBoot startup process, but looking back at the auto-configuration related sources, there are many that are not analyzed, so let's start with another wave of auto-configuration related sources.

As the author's level is limited, if there are any errors in this article, please point out that thank you.

Reference resources:
1,JSR-303

Welcome to the Public Number of Source Notes to learn and communicate.

Tags: Java Attribute Spring SpringBoot

Posted on Fri, 13 Mar 2020 10:14:38 -0700 by chixsilog