Analysis of @ Value source code in SpringBoot

1. Introduction

Before SpringBoot auto assembly In this article, I introduced the ConfigurationClassPostProcessor class, which is the basic intersection of SpringBoot as the extension of a series of Spring functions. Its derived ConfigurationClassParser is the basic processing class of parsing responsibilities, covering various parsing logic, such as @ Configuration, @ Bean, @ Import, @ ImportSource, @ PropertySource @ComponentScan and other annotations are completed in this parsing class. Since ConfigurationClassPostProcessor is the implementation class of BeanDefinitionRegistryPostProcessor, its parsing time is in the abstractapplicationcontext#invokebeanfactoryprocessors method and before processing beanfactoryprocessor.

The above annotations inject bean information into the Spring container. When we need to read the information of the configuration file, we need to use the @ Value or @ ConfigurationProperties annotation. Next, let's go deep into the source code and understand the implementation mechanism of @ Value.

2. Principle

Before exploring its implementation principle, we first locate keywords and then deduce the code logic. We pushed back by searching "Value.class":


Find a place that looks like a call point, enter qualifiernannotationautowirecandidateresolver and check the annotation description of its class:

/**
 * {@link AutowireCandidateResolver} implementation that matches bean definition qualifiers
 * against {@link Qualifier qualifier annotations} on the field or parameter to be autowired.
 * Also supports suggested expression values through a {@link Value value} annotation.
 *
 * <p>Also supports JSR-330's {@link javax.inject.Qualifier} annotation, if available.
 *
 * @author Mark Fisher
 * @author Juergen Hoeller
 * @author Stephane Nicoll
 * @since 2.5
 * @see AutowireCandidateQualifier
 * @see Qualifier
 * @see Value
 */

Roughly speaking, it is the implementation class of autowirecandidaterresolver, which is used to match the bean information required by the @ Qualifier annotation on the attribute or method parameters; It also supports expression parsing in @ Value annotation.

Therefore, we can be sure that qualifiernannotationautowirecandidateresolver is the processing class we are looking for, which is responsible for the Value taking operation of @ Qualifier and @ Value annotations. Next, let's look at the getSuggestedValue method for processing @ Value:

    @Override
    public Object getSuggestedValue(DependencyDescriptor descriptor) {
        // Find annotation information on attributes
        Object value = findValue(descriptor.getAnnotations());
        if (value == null) {
            MethodParameter methodParam = descriptor.getMethodParameter();
            if (methodParam != null) {
                // Find annotation information on method properties
                value = findValue(methodParam.getMethodAnnotations());
            }
        }
        return value;
    }

    /**
     * Determine a suggested value from any of the given candidate annotations.
     */
    protected Object findValue(Annotation[] annotationsToSearch) {
        if (annotationsToSearch.length > 0) {   // qualifier annotations have to be local
            // Find annotation information for @ Value
            AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes(
                    AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType);
            if (attr != null) {
                // Returns the expression in the annotation
                return extractValue(attr);
            }
        }
        return null;
    }

The purpose of this method is to obtain the expression in the @ Value annotation. The search range is on the properties and method parameters of the target class.

Now we need to solve two questions:

  1. Where is the value corresponding to the expression replaced?
  2. How does the value after expression replacement integrate with the original bean?

With these two questions, we continue to look for clues along the call stack and find that getSuggestedValue method is called by DefaultListableBeanFactory#doResolveDependency method:

public Object doResolveDependency(DependencyDescriptor descriptor, String beanName,
            Set<String> autowiredBeanNames, TypeConverter typeConverter) throws BeansException {

        InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
        try {
            Object shortcut = descriptor.resolveShortcut(this);
            if (shortcut != null) {
                return shortcut;
            }

            Class<?> type = descriptor.getDependencyType();
            // Get the expression in @ Value
            Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
            if (value != null) {
                if (value instanceof String) {
                    // When the expression is processed, the value of the expression will be replaced
                    String strVal = resolveEmbeddedValue((String) value);
                    BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null);
                    value = evaluateBeanDefinitionString(strVal, bd);
                }
                TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
                // Convert to the corresponding type and inject it into the original bean properties or method parameters
                return (descriptor.getField() != null ?
                        converter.convertIfNecessary(value, type, descriptor.getField()) :
                        converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
            }
            ...
        }
        finally {
            ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
        }
    }

Students who understand the getBean process in Spring should know that the DefaultListableBeanFactory#doResolveDependency is used to handle dependencies in beans. It can be seen that the time to process the @ Value annotation is to instantiate the bean in the getBean method, that is, the last step of Spring application #run.

After obtaining the expression in the @ Value annotation, enter the resolveEmbeddedValue method to replace the Value of the expression:

public String resolveEmbeddedValue(String value) {
        if (value == null) {
            return null;
        }
        String result = value;
        // Traverse StringValueResolver
        for (StringValueResolver resolver : this.embeddedValueResolvers) {
            result = resolver.resolveStringValue(result);
            if (result == null) {
                return null;
            }
        }
        return result;
    }

Through the code logic, we can see that the attribute resolution has been delegated to the corresponding implementation class of StringValueResolver. Next, we will analyze how the StringValueResolver is initialized.

2.1 initializing StringValueResolver

The implementation of the StringValueResolver function depends on Spring. The entry point is PropertySourcesPlaceholderConfigurer. Let's take a look at its structure.


Its key is to implement the beanfactory postprocessor interface, so as to use the external extension function postProcessBeanFactory to extend Spring:

public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        if (this.propertySources == null) {
            this.propertySources = new MutablePropertySources();
            if (this.environment != null) {
                this.propertySources.addLast(
                    new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
                        @Override
                        public String getProperty(String key) {
                            return this.source.getProperty(key);
                        }
                    }
                );
            }
            try {
                PropertySource<?> localPropertySource =
                        new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties());
                if (this.localOverride) {
                    this.propertySources.addFirst(localPropertySource);
                }
                else {
                    this.propertySources.addLast(localPropertySource);
                }
            }
            catch (IOException ex) {
                throw new BeanInitializationException("Could not load properties", ex);
            }
        }
        // Create replacement ${...} Expression processor
        processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources));
        this.appliedPropertySources = this.propertySources;
    }

The core step above is processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources)), where the processing ${...} will be created StringValueResolver of expression:

protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
            final ConfigurablePropertyResolver propertyResolver) throws BeansException {
        // Set prefix for placeholder: '{'
        propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
        // Set suffix for placeholder: '}'
        propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
        // Set default separator:
        propertyResolver.setValueSeparator(this.valueSeparator);
        // Build processing ${...} Expression processor
        StringValueResolver valueResolver = new StringValueResolver() {
            @Override
            public String resolveStringValue(String strVal) {
                String resolved = (ignoreUnresolvablePlaceholders ?
                        propertyResolver.resolvePlaceholders(strVal) :
                        propertyResolver.resolveRequiredPlaceholders(strVal));
                if (trimValues) {
                    resolved = resolved.trim();
                }
                return (resolved.equals(nullValue) ? null : resolved);
            }
        };
        // Put the processor into the Spring container
        doProcessProperties(beanFactoryToProcess, valueResolver);
    }

In the above code, resolvePlaceholders means to ignore if the variable cannot be resolved, and resolveRequiredPlaceholders means to throw an exception if the variable cannot be resolved (the default). Finally, store the generated StringValueResolver into the Spring container:

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) && beanFactoryToProcess.equals(this.beanFactory))) {
                BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
                try {
                    visitor.visitBeanDefinition(bd);
                }
                catch (Exception ex) {
                    throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
                }
            }
        }

        // New in Spring 2.5: resolve placeholders in alias target names and aliases as well.
        beanFactoryToProcess.resolveAliases(valueResolver);

        // Store StringValueResolver in BeanFactory
        beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
    }

Finally, register the StringValueResolver instance in the ConfigurableListableBeanFactory, that is, the StringValueResolver instance used when actually parsing variables.

After the resolveEmbeddedValue method, we get the replaced value, and then we integrate it with the original bean. The operation is in the TypeConverter#convertIfNecessary method. There are two cases:

  1. If the target class has an attribute decorated with @ Value.
    For example:

@Configuration
public class RedisProperties {
    @Value("${redis.url}")
    private String url;
    geter/setter...
}

In this case, the field of the target bean is called directly through reflection The set method (note that it is not the set method corresponding to the attribute) directly assigns a value to the attribute.

  1. If there is no @ Value decorated property in the target class.
    For example:

@Configuration
public class RedisProperties {
   
    @Value("${redis.url}")
    public void resolveUrl(String redisUrl){
      ...
      }
}

In this case, reflection is still used and method is called Invoke method to assign values to method parameters.

2.2 initialization of environment

There is a key point in this, which is a variable enviroment that is relied on when initializing MutablePropertySources. Enviroment is the basis for converting all Spring configuration files into KV, and the subsequent operations are further encapsulated on the basis of enviroment. Let's explore the initialization time of enviroment.

The initialization process of enviroment is not extended on the PostProcessor type extension interface, but through the configfileaolicationlistener listening mechanism. Let's look at its onApplicationEvent listening method:

public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent(
                    (ApplicationEnvironmentPreparedEvent) event);
        }
        if (event instanceof ApplicationPreparedEvent) {
            onApplicationPreparedEvent(event);
        }
    }

After loading the configuration file, SpringBoot will publish the ApplicationEnvironmentPreparedEvent event. After configfileaolicationlistener listens to the event, it will call onApplicationEnvironmentPreparedEvent method:

private void onApplicationEnvironmentPreparedEvent(
            ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        // Save configfileaolicationlistener to postProcessors
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        // Traverse the postProcessEnvironment method that executes the postprocessor
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessEnvironment(event.getEnvironment(),
                    event.getSpringApplication());
        }
    }

Since configfileaolicationlistener implements the EnvironmentPostProcessor, it is first incorporated into the postProcessors, and then traverses the postProcessors to execute its postProcessEnvironment method, so the ConfigFileApplicationListener#postProcessEnvironment method will be executed:

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment,
            SpringApplication application) {
        // Save the configuration file information into the environment
        addPropertySources(environment, application.getResourceLoader());
        configureIgnoreBeanInfo(environment);
        // Bind application context to Spring environment
        bindToSpringApplication(environment, application);
    }

The function of this method is to store the configuration file information in the environment and bind the environment with the Spring application context. We might as well go deep into the addPropertySources method and continue to explore the configuration file reading process. The core process is in configfileapplicationlistener In the loader #load() method:

    public void load() {
            this.propertiesLoader = new PropertySourcesLoader();
            this.activatedProfiles = false;
            this.profiles = Collections.asLifoQueue(new LinkedList<Profile>());
            this.processedProfiles = new LinkedList<Profile>();

            // You can mark different environments through profile by setting spring profiles. Active and spring profiles. default. 
            // If active is set, default will lose its function. If neither is set. Then the bean with profiles ID will not be created.
            Set<Profile> initialActiveProfiles = initializeActiveProfiles();
            this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
            if (this.profiles.isEmpty()) {
                for (String defaultProfileName : this.environment.getDefaultProfiles()) {
                    Profile defaultProfile = new Profile(defaultProfileName, true);
                    if (!this.profiles.contains(defaultProfile)) {
                        this.profiles.add(defaultProfile);
                    }
                }
            }

            // It supports the loading of bean s without adding any profile annotations
            this.profiles.add(null);

            while (!this.profiles.isEmpty()) {
                Profile profile = this.profiles.poll();
                // By default, SpringBoot finds application from four locations Properties / YML file
                // classpath:/,classpath:/config/,file:./,file:./config/
                for (String location : getSearchLocations()) {
                    if (!location.endsWith("/")) {
                        // location is a filename already, so don't search for more
                        // filenames
                        load(location, null, profile);
                    }
                    else {
                        for (String name : getSearchNames()) {
                            load(location, name, profile);
                        }
                    }
                }
                this.processedProfiles.add(profile);
            }

            addConfigurationProperties(this.propertiesLoader.getPropertySources());
        }

This involves the profile mechanism we used to use. Now most companies use the configuration center (such as apollo) to manage the configuration files uniformly. By default, SpringBoot finds application from four locations Properties / YML file: classpath:/,classpath:/config/,file:./,file:./config/.

2.3 registration of propertysourcesplaceholderconfigurer

As mentioned above, the implementation of the StringValueResolver function depends on Spring. The entry point is PropertySourcesPlaceholderConfigurer. When was it created?

We searched the call stack of this class and found that it was created in PropertyPlaceholderAutoConfiguration:

@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class PropertyPlaceholderAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

}

Yes, he was created through the automatic assembly feature of SpringBoot.

3. Summary

@The processor StringValueResolver of Value is initialized in PropertySourcesPlaceholderConfigurer#postProcessBeanFactory, while the @ Value property resolution is processed in the resolveDependency method of dependency processing in getBean.

4. Colored eggs

In addition to @ Value, you can also use @ ConfigurationProperties, which is unique to SpringBoot. For usage, readers can search online. I will only talk about the general principle here.

SpringBoot introduces configurationpropertiesbindingpostprocessorregister through the automatic assembly class ConfigurationPropertiesAutoConfiguration. It is the implementation class of ImportBeanDefinitionRegistrar. Its registerBeanDefinitions method will inject the bean information of configurationpropertiesbindingpostprocessor into the Spring container. ConfigurationPropertiesBindingPostProcessor is the implementation class of BeanPostProcessor, so it will call the AbstractApplicationContext#registerBeanPostProcessors method and register it as beanPostProcessors before bean instantiation (calling getBean). The postProcessBeforeInitialization method is called before the bean initialization, which parses the @ConfigurationProperties annotation, reads the corresponding configuration in enviroment, and binds with the current object.

Discuss the execution order of @ Value and @ Bean!
In this article, we know that the time to resolve the @ Value attribute is in the resolveDependency method of the configuration class to which @ Value belongs during getBean. The processing principle of @ Bean annotation is that in the invokebeanfactoryprocessors (beanfactory) method of refresh(), the method modified by @ Bean will be used as the factory method, Thus, a BeanDefinition information of its return Value type is generated and stored in the Spring container. When the Bean is instantiated, that is, in the createBeanInstance method of getBean, the instantiation operation will be carried out, and the method decorated with @ Bean will be called.

Therefore, the execution order of @ Value and @ bean depends on the loading order of the target class to which @ Value belongs and the return class of the @ bean modification method. By default, Spring loads these beans without dependencies in no order. If you want to interfere with their order, you must add some means, such as @ DependsOn.

However, if @ Value modifies the method of @ Bean, for example:

    @Bean
    @Value("${access.environment}") 
    public EnvironmentTool environmentTool(String env) {
        EnvironmentTool environmentTool = new EnvironmentTool();
        environmentTool.setEnv(env);
        return environmentTool;
    }

At this time, the target class to which @ Value belongs is the return class of the @ Bean modification method. In the createBeanInstance method of getBean, the instantiateUsingFactoryMethod method will be called when processing the factory method, and the resolveDependency method will be called at the bottom to process the filling logic of its properties, such as the processing logic of @ Value. Finally, the target method will be called through reflection, that is, the method logic modified by @ Bean. Therefore, when @ Value modifies the method of @ Bean, the processing time of @ Value is earlier than the method modified by @ Bean.


Transferred from: https://www.jianshu.com/p/933669270a9f

Keywords: Java Spring Spring Boot

Added by neox_blueline on Tue, 08 Feb 2022 18:11:15 +0200