写点什么

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

作者:洛神灬殇
  • 2022-12-19
    江苏
  • 本文字数:7812 字

    阅读完需:约 26 分钟

【深入浅出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 实现读取配置的。

  • PropertySourcesPlaceholderConfigurer PlaceholderConfigurerSupport 的特殊化实现。


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


PropertyPlaceholderConfigurer 和 PropertySourcesPlaceholderConfigurer 的执行目标

PropertyPlaceholderConfigurer PropertyPlaceholderConfigurer 在使用上并无本质的区别,两者的根本目标是将配置文件生成 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;// 见类 QualifierAnnotationAutowireCandidateResolverObject 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

  4. 通过 PropertySourcesPlaceholderConfigurer 寻找合适的 StringValueResolver 并解析得到 val 值。注入给 @Value 的 Field 或 Method。

  5. 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=1config.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; } }
@Servicepublic 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 + "]"; } }
复制代码


发布于: 2022-12-19阅读数: 19
用户头像

洛神灬殇

关注

🏆 InfoQ写作平台-签约作者 🏆 2020-03-25 加入

【个人简介】酷爱计算机科学、醉心编程技术、喜爱健身运动、热衷悬疑推理的“极客达人” 【技术格言】任何足够先进的技术都与魔法无异 【技术范畴】Java领域、Spring生态、MySQL专项、微服务/分布式体系和算法设计等

评论

发布
暂无评论
【深入浅出Spring原理及实战】「源码原理实战」从底层角度去分析研究PropertySourcesPlaceholderConfigurer的原理及实战注入机制_spring_洛神灬殇_InfoQ写作社区