SpringBoot 中 @Value 源碼解析

1、引言

在之前的《SpringBoot 自動裝配》文章中,我介紹了ConfigurationClassPostProcessor這個類,它是 SpringBoot 作為擴展 Spring 一系列功能的基礎路口,它所衍生的ConfigurationClassParser作為解析職責的基本處理類,涵蓋了各種解析處理的邏輯,如@Configuration、@Bean、@Import、@ImportSource、@PropertySource@ComponentScan等注解都在這個解析類中完成。由于ConfigurationClassPostProcessorBeanDefinitionRegistryPostProcessor的實現類,于是其解析時機是在AbstractApplicationContext#invokeBeanFactoryPostProcessors方法中,并且是在處理BeanFactoryPostProcessor之前。

以上的注解,都是將 bean 信息注入到 Spring 容器,那么當我們需要讀取配置文件的信息時,則需要使用到@Value或者@ConfigurationProperties注解。那么接下來,我們就深入源碼,了解@Value的實現機制。

2、原理

在探索它的實現原理之前,我們首先定位關鍵字然后反推代碼邏輯。我們通過搜索 "Value.class" 進行反推:


找到了一個看起來像是調用點的地方,進入QualifierAnnotationAutowireCandidateResolver,查看其類的注釋說明:


/**
 * {@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
 */

大致的意思是,它是AutowireCandidateResolver的實現類,用于匹配在屬性或者方法參數上的@Qualifier注解所需要的 bean 信息;同時支持@Value注解中的表達式解析。

于是我們可以肯定QualifierAnnotationAutowireCandidateResolver就是我們要找的處理類,它負責處理@Qualifier@Value兩個注解的取值操作。接下來我們看處理@ValuegetSuggestedValue方法:

    @Override
    public Object getSuggestedValue(DependencyDescriptor descriptor) {
        // 在屬性上查找注解信息
        Object value = findValue(descriptor.getAnnotations());
        if (value == null) {
            MethodParameter methodParam = descriptor.getMethodParameter();
            if (methodParam != null) {
                // 在方法屬性上查找注解信息
                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
            // 查找 @Value 的注解信息
            AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes(
                    AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType);
            if (attr != null) {
                // 返回注解中的表達式
                return extractValue(attr);
            }
        }
        return null;
    }

該方法的目的是獲取@Value注解中的表達式,查找范圍是在目標類的屬性和方法參數上。

現在要解決兩個疑問:

  1. 表達式對應的值是在哪里被替換的?
  2. 表達式替換后的值又是如何與原有的 bean 整合的?

帶著這兩個疑問,我們順著調用棧繼續查找線索,發現getSuggestedValue方法是被DefaultListableBeanFactory#doResolveDependency方法調用了:

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();
            // 獲取 @Value 中的表達式
            Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
            if (value != null) {
                if (value instanceof String) {
                    // 處理表達式,這里就會替換表達式的值
                    String strVal = resolveEmbeddedValue((String) value);
                    BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null);
                    value = evaluateBeanDefinitionString(strVal, bd);
                }
                TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
                // 轉換為對應的類型,并且注入原有 bean 屬性或者方法參數中
                return (descriptor.getField() != null ?
                        converter.convertIfNecessary(value, type, descriptor.getField()) :
                        converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
            }
            ...
        }
        finally {
            ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
        }
    }

了解 Spring 中getBean流程的同學應該知道,DefaultListableBeanFactory#doResolveDependency作用是處理 bean 中的依賴。由此可見,處理@Value注解的時機是在getBean方法中,即SpringApplication#run的最后一步,實例化 bean。

當獲取@Value注解中的表達式之后,進入了resolveEmbeddedValue方法,來替換表達式的值:

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

通過代碼邏輯我們看到,對于屬性的解析已經委托給了StringValueResolver對應的實現類,接下來我們就要分析一下這個StringValueResolver是如何初始化的。

2.1 初始化 StringValueResolver

StringValueResolver功能實現依賴 Spring 的切入點是PropertySourcesPlaceholderConfigurer,我們看一下它的結構。


它的關鍵是實現了BeanFactoryPostProcessor接口,從而利用實現對外擴展函數postProcessBeanFactory來進行對 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);
            }
        }
        // 創建替換 ${...} 表達式的處理器
        processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources));
        this.appliedPropertySources = this.propertySources;
    }

上面的核心步驟是processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources)),這里會創建處理 ${...} 表達式的StringValueResolver:

protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
            final ConfigurablePropertyResolver propertyResolver) throws BeansException {
        // 設置占位符的前綴:"{"
        propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
        // 設置占位符的后綴:"}"
        propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
        // 設置默認值分隔符:":"
        propertyResolver.setValueSeparator(this.valueSeparator);
        // 生成處理 ${...} 表達式的處理器
        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);
            }
        };
        // 將處理器放入 Spring 容器
        doProcessProperties(beanFactoryToProcess, valueResolver);
    }

在上面的代碼中,resolvePlaceholders表示如果變量無法解析則忽略,resolveRequiredPlaceholders表示如果變量無法解析則拋異常(默認情況)。最后將生成的StringValueResolver存入 Spring 容器中:

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);

        // 將 StringValueResolver 存入 BeanFactory 中
        beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
    }

最后將StringValueResolver實例注冊到ConfigurableListableBeanFactory中,也就是在真正解析變量時使用的StringValueResolver實例。

經過resolveEmbeddedValue方法之后,我們就拿到了替換后的值,接下來就是與原 bean 進行整合了。其操作是在TypeConverter#convertIfNecessary方法中,這里分為兩種情況:

  1. 如果目標類存在@Value修飾的屬性。
    如:
@Configuration
public class RedisProperties {
    @Value("${redis.url}")
    private String url;
    geter/setter...
}

該情況直接通過反射調用目標 bean 的Field.set方法(注意,不是屬性對應的 set 方法),直接給屬性賦值。

  1. 如果目標類不存在@Value修飾的屬性。
    如:
@Configuration
public class RedisProperties {
   
    @Value("${redis.url}")
    public void resolveUrl(String redisUrl){
      ...
      }
}

該情況依舊使用反射,調用Method.invoke方法,給方法參數進行賦值。

2.2 Enviroment 的初始化

這里面有一個關鍵點,就是在初始化MutablePropertySources的時候依賴的一個變量enviroment。Enviroment 是 Spring 所有配置文件轉換為 KV 的基礎,而后續的一系列操作都是在enviroment基礎上做的進一步封裝,那么我們就來探索一下enviroment的初始化時機。

enviroment的初始化過程并不是之前通用的在 PostProcessor 類型的擴展接口上做擴展,而是通過ConfigFileAoolicationListener監聽機制完成的。我們看其onApplicationEvent監聽方法:

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

當加載完成配置文件之后,SpringBoot 就會發布ApplicationEnvironmentPreparedEvent事件,ConfigFileAoolicationListener監聽到該事件之后,就會調用onApplicationEnvironmentPreparedEvent方法:

private void onApplicationEnvironmentPreparedEvent(
            ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        // 將 ConfigFileAoolicationListener 存入 postProcessors
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        // 遍歷執行 EnvironmentPostProcessor 的 postProcessEnvironment 方法
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessEnvironment(event.getEnvironment(),
                    event.getSpringApplication());
        }
    }

由于ConfigFileAoolicationListener實現了EnvironmentPostProcessor,于是這里首先將其納入postProcessors,然后遍歷postProcessors,執行其postProcessEnvironment方法,于是ConfigFileApplicationListener#postProcessEnvironment方法就會被執行:

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment,
            SpringApplication application) {
        // 將配置文件信息存入 environment
        addPropertySources(environment, application.getResourceLoader());
        configureIgnoreBeanInfo(environment);
        // 將 environment 與 Spring 應用上下文綁定
        bindToSpringApplication(environment, application);
    }

該方法的作用是將配置文件信息存入environment,并將environment與 Spring 應用上下文進行綁定。我們不妨深入addPropertySources方法,繼續探討配置文件讀取流程,其核心流程是在ConfigFileApplicationListener.Loader#load()方法中:

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

            // 通過 profile 標記不同的環境,可以通過設置 spring.profiles.active 和 spring.profiles.default。
            // 如果設置了 active,default 便失去了作用。如果兩個都沒設置。那么帶有 profiles 標識的 bean 不會被創建。
            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);
                    }
                }
            }

            // 支持不添加任何 profile 注解的 bean 的加載
            this.profiles.add(null);

            while (!this.profiles.isEmpty()) {
                Profile profile = this.profiles.poll();
                // SpringBoot 默認從 4 個位置查找 application.properties/yml 文件
                // 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());
        }

這里涉及到我們以前經常用的 profile 機制,現在大部分公司都是使用配置中心(如 apollo)對配置文件統一管理的。SpringBoot 默認從 4 個位置查找 application.properties/yml 文件:classpath:/,classpath:/config/,file:./,file:./config/。

2.3 PropertySourcesPlaceholderConfigurer 的注冊

上面提到StringValueResolver功能實現依賴 Spring 的切入點是PropertySourcesPlaceholderConfigurer,那么它又是何時創建的呢?

我們搜索該類的調用棧,發現其在PropertyPlaceholderAutoConfiguration中創建的:

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

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

}

沒錯,他就是通過 SpringBoot 的自動裝配特性創建的。

3. 小結

@Value的處理器StringValueResolver初始化時機是PropertySourcesPlaceholderConfigurer#postProcessBeanFactory中,而處理@Value屬性解析的時機是在getBean中的依賴處理resolveDependency方法中。

4. 彩蛋

獲取配置文件信息除了@Value以外,還可以使用@ConfigurationProperties,它是 SpringBoot 特有的,關于用法讀者自己網上去搜,我這里只講大概的原理。

SpringBoot 通過自動裝配類ConfigurationPropertiesAutoConfiguration引入了ConfigurationPropertiesBindingPostProcessorRegistrar,它是ImportBeanDefinitionRegistrar的實現類,其registerBeanDefinitions方法會將ConfigurationPropertiesBindingPostProcessor的 bean 信息注入 Spring 容器。而ConfigurationPropertiesBindingPostProcessorBeanPostProcessor的實現類,于是會在 bean 實例化(調用 getBean )之前,調用AbstractApplicationContext#registerBeanPostProcessors方法,將其注冊為beanPostProcessors。于是會在 bean 初始化之前,調用postProcessBeforeInitialization方法,該方法會解析@ConfigurationProperties注解,讀取enviroment中的對應的配置,并且與當前對象進行綁定。

探討下@Value@Bean的執行先后順序!
在本文中,我們知道@Value屬性解析的時機是在@Value所屬的配置類在進行getBean時的依賴處理resolveDependency方法中,而@Bean注解的處理原理是,在refresh()時的invokeBeanFactoryPostProcessors(beanFactory)方法中,會根據@Bean修飾的方法作為factory-method(工廠方法),從而生成一個其返回值類型的BeanDefinition信息,并且存入 Spring 容器中。在該 Bean 實例化的時候,即在getBean時的createBeanInstance方法中,會進行實例化操作,就會調用@Bean修飾的方法。

于是@Value@Bean的執行先后順序,取決于@Value所屬的目標類@Bean修飾方法的返回類的加載先后順序,而 Spring 默認情況下,加載這些沒有依賴關系的 bean 是沒有順序的。要想干預他們的順序,就必須加一些手段了,比如@DependsOn。

但是如果@Value修飾的是@Bean的方法,比如:

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

此時@Value所屬的目標類為@Bean修飾方法的返回類,由于getBeancreateBeanInstance方法中,在處理factory-method的時候,會調用instantiateUsingFactoryMethod方法,其底層會調用resolveDependency方法來處理其屬性的填充邏輯,比如@Value的處理邏輯。最后會通過反射調用目標方法,即@Bean修飾的方法邏輯。所以,當@Value修飾的是@Bean的方法時,@Value的處理時機是早于@Bean所修飾的方法的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容