【深入浅出Spring原理及实战】「源码原理实战」从底层角度去分析研究PropertySourcesPlaceholderConfigurer的原理及实战注入机制

Spring提供配置解析功能

主要有一下xml文件占位符解析和Java的属性@Value的占位符解析配置这两种场景进行分析和实现解析,如下面两种案例。

xml文件的占位符解析配置

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"  init-method="init" destroy-method="close">
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

Java的属性@Value的占位符解析配置

@Value 注解值进行属性占位符解析和替换

@Value("${config}")
private String config;

PropertyPlaceholderConfigurer和PropertySourcesPlaceholderConfigurer

通过配置xml来实现对Classpath下的配置文件的占位符的属性进行注入,或者实现Java的属性@Value的占位符解析配置。

  • 在Spring3.1版本之前是通过PropertyPlaceholderConfigurer实现的。
  • 在Spring3.1之后则是通过PropertySourcesPlaceholderConfigurer实现的。

注意:在Spring Context 3.1或者更高版本中,缺省使用PropertySourcesPlaceholderConfigurer工具替换了PlaceholderConfigurerSupport,而<=3.0较老的Spring Context中,为了保持和之前的版本兼容,缺省还是使用PropertyPlaceholderConfigurer

PropertyPlaceholderConfigurer和PropertySourcesPlaceholderConfigurer的实现分析

  • PropertyPlaceholderConfigurer本质是基于PlaceholderConfigurerSupport实现读取配置的。
  • PropertySourcesPlaceholderConfigurerPlaceholderConfigurerSupport的特殊化实现。

下图介绍对应的配置解析的继承关系图谱。


image

PropertyPlaceholderConfigurer和PropertySourcesPlaceholderConfigurer的执行目标

PropertyPlaceholderConfigurerPropertyPlaceholderConfigurer在使用上并无本质的区别,两者的根本目标是将配置文件生成KV对,真正的注入工作并不由它们本身执行。

PropertySourcesPlaceholderConfigurer它用于解析bean定义中的属性值,以及注解@Value的值,使用的属性来源是当前的Spring Environment对象,以及设置给自己的PropertySources对象。

Spring Boot 自动配置类 PropertyPlaceholderAutoConfiguration
@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class PropertyPlaceholderAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

PropertyPlaceholderAutoConfiguration定义一个PropertySourcesPlaceholderConfigurer bean,该bean作为一个BeanFactoryPostProcessor,会在容器启动时容器后置处理阶段执行自己的任务。BeanFactoryPostProcessor的优先级又优于其余的Bean。因此可以实现在bean初始化之前的注入。

postProcessBeanFactory方法的执行

如果外部指定了this.propertySources, 则直接使用它,否则从当前Spring的Environment 对象和自身的 #mergeProperties 方法调用返回的 Properties 对象构建属性源对象 this.propertySources

    @Override
    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
                        @Nullable
                        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;
    }

构造一个基于特定属性源 this.propertySources 对属性值进行解析的属性值解析器PropertySourcesPropertyResolver, 对容器中所有的 bean 定义中的属性值,构造函数参数值。

    /**
     * Visit each bean definition in the given bean factory and attempt to replace ${...} property
     * placeholders with values from the given properties.
     */
    protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
            final ConfigurablePropertyResolver propertyResolver) throws BeansException {
       // 设置属性值解析器所使用的占位符格式参数,缺省为:
       // 占位符前缀 ${
        propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
       // 占位符后缀 }
        propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
       // 缺省值分隔符 :
        propertyResolver.setValueSeparator(this.valueSeparator);
       // 结合属性 this. ignoreUnresolvablePlaceholders对propertyResolver 作进一步封装,
       // 封装出来一个 StringValueResolver valueResolver,这是最终要应用的属性值解析器
        StringValueResolver valueResolver = strVal -> {
            String resolved = (this.ignoreUnresolvablePlaceholders ?
                    propertyResolver.resolvePlaceholders(strVal) :
                    propertyResolver.resolveRequiredPlaceholders(strVal));
            if (this.trimValues) {
                resolved = resolved.trim();
            }
            return (resolved.equals(this.nullValue) ? null : resolved);
        };
       // 调用基类PlaceholderConfigurerSupport实现的对容器中所有 bean定义进行遍历处理属性值中占位符解析的逻辑
        doProcessProperties(beanFactoryToProcess, valueResolver);
    }

doProcessProperties的方法目的是为了添加解析器StringValueResolver

    protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
            StringValueResolver valueResolver) {
        // ignore
        ....

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

        // New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
        beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
    }

这里的ddEmbeddedValueResolver(StringValueResolver) 是为一个 LinkedList添加值。在取用的时候是优先从链表头开始取用的。 一旦发现无法找到值,直接就抛异常了。这个就对外体现出 PropertySourcesPlaceholderConfigurer 的唯一性。

然而Spring内部还是有多个PropertySourcesPlaceholderConfigurer, 只不过除了排列在队首的 PropertySourcesPlaceholderConfigurer之外全都被忽略掉了。

PropertySourcesPlaceholderConfigurer属性注入的原理

AbstractApplicationContext#obtainFreshBeanFactory

Spring框架进行植入元素注入时机

针对于元素的注入依赖于
AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues1。

AbstractApplicationContext#finishBeanFactoryInitialization方法

在Spring初始化流程中,执行AbstractApplicationContext#finishBeanFactoryInitialization方法。 该方法里面发生的主要流程为Spring业务Bean初始化。 实际流程跟Spring Bean的初始化没有任务区别。

InstantiationAwareBeanPostProcessor
  • 通过对接口 InstantiationAwareBeanPostProcessor 实现类的方法进行执行。 仅此而已。
AutowiredAnnotationBeanPostProcessor
  • InjectionMetadataInjectionMetadataInjectedElementInjectedElement这个类是 InstantiationAwareBeanPostProcessor的一个实现类。
@Value和@Autowired注解实际执行
  1. 用于@Value和@Autowired注解实际执行方法postProcessPropertyValues调度实际调度InjectedElement子类被注入值的获取来自于DefaultListableBeanFactory将对应@Value(“${configValue}”)里面的值替换的来源值,是PropertySourcesPlaceholderConfigurer生成的StringValueResolver。

  2. Spring原生的Bean是单例的它直接被储存在了AbstractBeanFactory执行Field.set(Object, Object)或者Method.invoke(Object, Object[])。

所以,可以看出 PropertySourcesPlaceholderConfigurer 或者 PropertyPlaceholderConfigurer仅仅是做了一个配置文件的解析工作,真正的注入并不由它们完成,而是托付给了Spring 的Bean初始化流程。这两个类实现了BeanFactoryPostProcessor 接口,这个接口的优先级高于后续的Spring Bean。

通过解析了的PropertySourcesPlaceholderConfigurer查询得到元素值。 没有则抛出异常,如下源码:

DefaultListableBeanFactory#doResolveDependency

@Value 注解值进行属性占位符解析和替换

// 获取注解的 value() 值。被写死为 Class<? extends Annotation> valueAnnotationType = Value.class;
// 见类 QualifierAnnotationAutowireCandidateResolver
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
    if (value instanceof String) {
        // 通过PropertySourcesPlaceholderConfigurer写入的键值对元素获取元素的值.
        // 方法内注册了多个StringValueResolver,循环查找值。提供者为PropertySourcesPlaceholderConfigurer,因此配置多个解析器的时候是以最后的配置为准的。
        String strVal = resolveEmbeddedValue((String) value);
        BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null);
        value = evaluateBeanDefinitionString(strVal, bd);
    }
    TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
    return (descriptor.getField() != null ?
            converter.convertIfNecessary(value, type, descriptor.getField()) :
            converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
}

读取配置的方式介绍

xml文件读取配置信息案例

通过PropertyPlaceholderConfigurer进行配置Bean方式

单个配置文件。

<bean id="propertyConfigurer"class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
   <property name="location">
     <value>conf/sqlmap/jdbc.properties</value>
   </property>
    <property name="fileEncoding">
      <value>UTF-8</value>
    </property>
</bean>

多个配置文件

注意这两种value值的写法

<bean id="propertyConfigurer"class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">  
    <property name="locations">
        <list>
            <value>/WEB-INF/mail.properties</value>  
            <value>classpath: conf/sqlmap/jdbc.properties</value>
     </list>
    </property>
</bean>

Spring标签方式

<context:property-placeholder location="classpath*:/WEB-INF/mail.properties" />

这总方式的原理就是构造一个PropertySourcesPlaceholderConfigurer, (3.1之前是PropertyPlaceholderConfigurer)

  • ContextNamespaceHandler#init
  • PropertyPlaceholderBeanDefinitionParser#doParse
注入配置触发点

Spring初始化Context的时候读取XML配置, 这个流程优先于Spring普通Bean初始化。配合扫包(<context:component-scan />)得到的Bean进而实现对XML里面配置的Bean的载入。

  • PropertySourcesPlaceholderConfigurer本质上是一个BeanFactoryPostProcessor。解析XML的流程在BeanFactoryPostProcessor之前, 优先将配置文件的路径以及名字通过Setter传入PropertySourcesPlaceholderConfigurer。

总结Spring Value注入流程

构建PropertySourcesPlaceholderConfigurerBean或者PropertyPlaceholderConfigurerBean的组件

  1. 配置Spring @Value("val2Inject") 方式获取配置文件的属性,需要依赖于在Spring XML里面配置<context:property-placeholder /> 或者PropertySourcesPlaceholderConfigurerBean来添加配置文件的名称。

  2. 读取到context:property-placeholder标签或者PropertySourcesPlaceholderConfigurer
    解析并实例化一个PropertySourcesPlaceholderConfigurer。同时向其中注入配置文件路径、名称PropertySourcesPlaceholderConfigurer自身生成多个StringValueResolver备用,Bean准备完毕。

  3. Spring在初始化非BeanFactoryPostProcessor的Bean的时候,AutowiredAnnotationBeanPostProcessor负责找到Bean内有@Value注解的Field或者Method

    • 通过PropertySourcesPlaceholderConfigurer寻找合适的StringValueResolver并解析得到val值。注入给@Value的Field或Method。
  4. AutowiredAnnotationBeanPostProcessor负责@Autowired和@Value两个注解的解析。

@PropertySource注解配置读取单个或多个配置文件

单个配置文件:

@PropertySource(value = "classpath:config/application-config.properties")

多个配置文件:

@PropertySource(value = {"classpath:config/application-config1.properties","classpath:config/application-config2.properties"})

@PropertySource注解使用有两种方式

  1. @PropertySource + Environment,通过@PropertySource注解将properties配置文件中的值存储到Spring的Environment中,Environment接口提供方法去读取配置文件中的值,参数是properties文件中定义的key值。

  2. @PropertySource(PropertySourcesPlaceholderConfigurer) +@Value

@PropertySource + Environment

@Configuration
@ComponentScan(basePackages = "com.libo.config")
@PropertySource(value = "classpath:config/application-config.properties")
public class TestPropertieEnvironment { 

    @Autowired
    Environment environment;
    public String properties(){
        String key = this.environment.getProperty("config.key");
        System.out.println(key);
        return null;
    }
}
配置文件config.properties:
config.key=1
config.value=2
测试类操作
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(TestPropertieEnvironment.class);
        ServiceConfiguration hc2 = (TestPropertieEnvironment) context.getBean("testPropertieEnvironment");
        hc2.properties();
    }
}

@PropertySource(PropertySourcesPlaceholderConfigurer)+@Value

PropertySourcesPlaceholderConfigurer是PlaceholderConfigurerSupport的特殊化实现。它用于解析bean定义中的属性值,以及注解@Value的值,使用的属性来源是当前的Spring Environment对象,以及设置给自己的PropertySources对象。

  • 大于3.1更高版本中,缺省使用该工具替换了PlaceholderConfigurerSupport

  • <=3.0较老的Spring中,为了保持和之前的版本兼容,缺省还是使用PropertyPlaceholderConfigurer。

创建PropertySourcesPlaceholderConfigurer
创建PropertiesConfig
@Component
@PropertySource(value = "classpath:config/application-config.properties")
public class PropertiesConfig {
    @Value("${config.value}")
    private String value;
    @Value("${config.key}")
    private String key;
 
}

测试类忽略!

自定义PropertyPlaceholderConfigurer

@Configuration
@ComponentScan(basePackages = "com.libo.config")
public class PropertiesConfiguration2 { 

    @Bean 
    public static PropertyPlaceholderConfigurer configurer() { 
         PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
         Resource resources = new ClassPathResource( "config/appplication-config.properties" );
         ppc.setLocation(resources);
         return ppc; 
    } 
    
    @Bean
    public Configs2 configs2(@Value("${ds.user}") String user, @Value("${key1}") String key1) {
        Configs2 configs = new Configs2();
        configs.setApiKeyId(user);
        configs.setSecretApiKey(key1);
        System.out.println("in ServiceConfiguration" + configs);
        return configs;
    }
    
}

@Service
public class TestConfigs2 {

    @Autowired
    Configs2 configs2;
    
    @Autowired
    Configs configs;
    
    public void testConfigs2() {
        System.out.println("configs:"+configs.getApiKeyId());
        System.out.println("configs2:"+configs2.getApiKeyId());
    }
}

测试类

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ServiceConfiguration2.class);       
        TestConfigs2 hc2 = (TestConfigs2) context.getBean("testConfigs2");
        hc2.testConfigs2();
    }
}

此外需要注意的是:PropertySource是可以支持ignoreResourceNotFound支持无法获取配置文件的i情况。

Spring4版本的PropertySources的注解

在Spring 4版本中,Spring提供了一个新的注解——@PropertySources,从名字就可以猜测到它是为多配置文件而准备的。

@PropertySources({
//@PropertySource("classpath:db.properties"),
@PropertySource(value="classpath:db.properties", ignoreResourceNotFound=true),
@PropertySource("classpath:spring/config.properties")
    public class AppConfig {
    @Value("${key1}")
    private String key1;
    
    @Value("${key2}")
    private String key2;

    @Override
    public String toString() {
        return "AppConfig [key1=" + key1 + ", key2=" + key2 + "]";
    } 
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容