< context:property-placeholder/>标签以及PropertySourcesPlaceholderConfigurer占位符解析器源码深度解析【一万字】

  基于最新Spring 5.x,详细介绍了< context:property-placeholder/>扩展标签的解析,以及PropertySourcesPlaceholderConfigurer占位符解析器的创建以及工作流程源码。

  简单的说,PropertySourcesPlaceholderConfigurer就是用来解析、替换bean定义内的属性值和@Resource、@Value等依赖注入注解的属性值中的${… : …}占位符的一个工具类。本次我们讲解PropertySourcesPlaceholderConfigurer的实例化来源以及工作流程的源码。
  本文涉及到一些IoC容器初始化的整体流程,比如扩展标签解析、BeanFactoryPostProcessor的初始化和回调、普通bean实例化,以及这些步骤之间的先后顺序,以及各种Spring的扩展点,比如BeanFactoryPostProcessor、Aware接口以及它们的回调时机。这些知识点本文不会讲解,默认认为大家知道,仅仅是讲解与PropertySourcesPlaceholderConfigurer相关联的知识。如果此前你较好的掌握了IoC容器初始化流程,那么对于我上面说的这些应该不会很陌生,但是如果你没掌握,那么直接阅读本文将会给你带来很大的困扰,如果你想要详细了解IoC容器初始化流程及其相关知识点,那么可以看我的专栏文章,这将会耗费你一定的时间Spring 5.x 源码。

Spring IoC容器初始化源码 系列文章

Spring IoC容器初始化源码(1)—setConfigLocations设置容器配置信息

Spring IoC容器初始化源码(2)—prepareRefresh准备刷新、obtainFreshBeanFactory加载XML资源、解析<beans/>标签

Spring IoC容器初始化源码(3)—parseDefaultElement、parseCustomElement解析默认、扩展标签,registerBeanDefinition注册Bean定义

Spring IoC容器初始化源码(4)—<context:component-scan/>标签解析、spring.components扩展点、自定义Spring命名空间扩展点

Spring IoC容器初始化源码(5)—prepareBeanFactory、invokeBeanFactoryPostProcessors、registerBeanPostProcessors方法

Spring IoC容器初始化源码(6)—finishBeanFactoryInitialization实例化Bean的整体流程以及某些扩展点

Spring IoC容器初始化源码(7)—createBean实例化Bean的整体流程以及构造器自动注入

Spring IoC容器初始化源码(8)—populateBean、initializeBean实例化Bean以及其他依赖注入

< context:property-placeholder/>标签以及PropertySourcesPlaceholderConfigurer占位符解析器源码深度解析

三万字的ConfigurationClassPostProcessor配置类后处理器源码深度解析

基于JavaConfig的AnnotationConfigApplicationContext IoC容器初始化源码分析

文章目录

  • Spring IoC容器初始化源码 系列文章
  • 1 < context:property-placeholder/>扩展标签解析
    • 1.1 AbstractBeanDefinitionParser.parse解析标签
      • 1.1.1 AbstractSingleBeanDefinitionParser.parseInternal解析获取bean定义
        • 1.1.1.1 getBeanClass获取bean定义的class类型
        • 1.1.1.2 doParse解析自有属性
    • 1.2 parse方法小结
  • 2 PropertySourcesPlaceholderConfigurer工作流程
    • 2.1 postProcessBeanFactory加载属性源、解析占位符
      • 2.1.1 mergeProperties合并本地属性源
      • 2.1.2 processProperties替换占位符
        • 2.1.2.1 doProcessProperties替换占位符
    • 2.2 postProcessBeanFactory方法小结
  • 3 多个< context:property-placeholder/>配置
    • 3.1 测试案例
  • 4 PropertySourcesPlaceholderConfigurer总结

1 < context:property-placeholder/>扩展标签解析

  PropertySourcesPlaceholderConfigurer对象并不是凭空产生的,它一定有一个实例化的流程(废话)。我们可以直接在XML中或者通过@Bean手动设置一个PropertySourcesPlaceholderConfigurer实例给IoC容器,这种方式比较简单。本次我们讲解另一种“隐式”的初始化流程,那就是配置< context:property-placeholder/>扩展标签。
  首先,我们需要明白< context:property-placeholder/>扩展标签的作用就是用于引入本地属性配置文件,常见的就是xxx.properties。该标签被加载、解析成为一个PropertySourcesPlaceholderConfigurer类型的bean定义(Spring3.1之后默认),在后续实例化这个bean定义之后,就能通过environment和本地配置文件提供的属性源来解析替换bean定义内的属性值和@Resource、@Value等依赖注入注解的属性值中的${… : …}占位符,从而实现配置分离! 而容器构造器中的资源路径占位符、< import/>标签中的占位符、< context:component-scan/>标签内部的占位符等等其它标签的占位符在解析时,只会使用environment中的属性源。
  此前在IoC容器初始化源码中我们讲过标签元素的解析是在refresh方法中的obtainFreshBeanFactory方法中的中完成的,而扩展标签的解析,实际上是由每一个标签对应的解析器的parse方法来完成的,< context:property-placeholder/>扩展标签是使用PropertyPlaceholderBeanDefinitionParser解析器来解析的。这个类的调用层级比较深调用,它的parse方法以及很多方法都是复用的父类AbstractBeanDefinitionParser的通用parse方法的逻辑。
< context:property-placeholder/>标签以及PropertySourcesPlaceholderConfigurer占位符解析器源码深度解析【一万字】_第1张图片

1.1 AbstractBeanDefinitionParser.parse解析标签

  AbstractBeanDefinitionParser是扩展标签的通用抽象解析器,定义了解析规则骨架。它的通用parse方法,基于模版方法模式,BeanDefinitionParser的parse方法(扩展标签的解析方法)的抽象骨干实现。
  该方法解析对应的标签成为bean定义,并且会通过registerBeanDefinition向工厂中注册对应的bean定义。

  1. 调用parseInternal方法将element解析为单个BeanDefinition对象;
  2. 解析id属性,如果需要Spring自动生成id,那么使用DefaultBeanNameGenerator生成器;
  3. 根据name属性,生成别名aliases数组;
  4. 将bean定义、id、aliases封装成为BeanDefinitionHolder,随后调用registerBeanDefinition注册;
  5. 发布组件注册事件。
//--------AbstractBeanDefinitionParser的相关属性--------

/**
 * "id"属性的常量
 */
public static final String ID_ATTRIBUTE = "id";

/**
 * "name"属性的常量
 */
public static final String NAME_ATTRIBUTE = "name";


/**
 * AbstractBeanDefinitionParser的方法
 * 

* 基于模版方法模式,BeanDefinitionParser的parse方法(扩展标签的解析方法)的抽象骨干实现 * 1 将element解析为单个BeanDefinition对象 * 2 解析id属性,如果需要Spring自动生成id,那么使用DefaultBeanNameGenerator生成器 * 3 根据name属性,生成别名aliases数组 * 4 将bean定义、id、aliases封装成为BeanDefinitionHolder,随后调用registerBeanDefinition注册 * 5 发布组件注册事件 * * @param element 标签元素 * @param parserContext 解析上下文 * @return 解析后的bean定义 */ @Override @Nullable public final BeanDefinition parse(Element element, ParserContext parserContext) { /* * 1 调用parseInternal方法解析标签获取bean定义,这个方法是子类实现的 */ AbstractBeanDefinition definition = parseInternal(element, parserContext); if (definition != null && !parserContext.isNested()) { try { /* * 2 解析id属性,如果需要Spring自动生成,那么使用DefaultBeanNameGenerator生成器 */ String id = resolveId(element, definition, parserContext); if (!StringUtils.hasText(id)) { parserContext.getReaderContext().error( "Id is required for element '" + parserContext.getDelegate().getLocalName(element) + "' when used as a top-level tag", element); } /* * 3 别名处理 */ String[] aliases = null; //是否应该解析name属性作为别名,默认true if (shouldParseNameAsAliases()) { //获取name属性 String name = element.getAttribute(NAME_ATTRIBUTE); //如果具有该属性 if (StringUtils.hasLength(name)) { //将name属性值按照","分隔成为一个数组,去除前后空白,作为别名 aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name)); } } BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases); /* * 4 注册bean定义,registerBeanDefinition方法我们此前讲过了 */ registerBeanDefinition(holder, parserContext.getRegistry()); /* * 5 发布组件注册事件 */ //是否应该发布事件,默认true if (shouldFireEvents()) { BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder); postProcessComponentDefinition(componentDefinition); //发布组件注册事件 parserContext.registerComponent(componentDefinition); } } catch (BeanDefinitionStoreException ex) { String msg = ex.getMessage(); parserContext.getReaderContext().error((msg != null ? msg : ex.toString()), element); return null; } } //返回bean定义 return definition; }

1.1.1 AbstractSingleBeanDefinitionParser.parseInternal解析获取bean定义

  AbstractSingleBeanDefinitionParser是单个bean定义解析规则的抽象实现。
  parseInternal方法基于建造者模式和模版方法模式,通过BeanDefinitionBuilder以及标签元素的各种属性构建一个GenericBeanDefinition的骨干实现,不同标签的自有属性的解析是通过内部的doParse方法解析的,这个方法由具体的解析器子类实现。

/**
 * AbstractSingleBeanDefinitionParser的方法
 * 

* 基于建造者模式和模版方法模式 * 通过BeanDefinitionBuilder以及标签元素的各种属性构建一个GenericBeanDefinition的骨干实现 * 不同标签的自有属性的解析是通过内部的doParse方法解析的,这个方法由具体的解析器子类实现 * * @param element 要解析为单个 Bean 定义的标签元素 * @param parserContext 解析上下文 * @return 由提供的element解析的BeanDefinition */ @Override protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { //获取一个builder BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(); //获取element的parentName属性,默认返回null,子类PropertyPlaceholderBeanDefinitionParser没有重写 String parentName = getParentName(element); if (parentName != null) { builder.getRawBeanDefinition().setParentName(parentName); } /* * 获取element的Class,默认返回null,子类PropertyPlaceholderBeanDefinitionParser重写了该方法 * 默认返回PropertySourcesPlaceholderConfigurer.class */ Class<?> beanClass = getBeanClass(element); if (beanClass != null) { builder.getRawBeanDefinition().setBeanClass(beanClass); } else { String beanClassName = getBeanClassName(element); if (beanClassName != null) { builder.getRawBeanDefinition().setBeanClassName(beanClassName); } } //设置源 builder.getRawBeanDefinition().setSource(parserContext.extractSource(element)); //内部bean设置为和外部bean的相同的scope作用域属性 BeanDefinition containingBd = parserContext.getContainingBeanDefinition(); if (containingBd != null) { // Inner bean definition must receive same scope as containing bean. builder.setScope(containingBd.getScope()); } //判断< beans/>的default-lazy-init属性,即是否延迟初始化 if (parserContext.isDefaultLazyInit()) { // Default-lazy-init applies to custom bean definitions as well. builder.setLazyInit(true); } /* * 调用doParse方法继续解析 * 不同标签的自有属性的解析是通过内部的doParse方法解析的,这个方法由具体的解析器子类实现 */ doParse(element, parserContext, builder); //构建一个GenericBeanDefinition return builder.getBeanDefinition(); }

1.1.1.1 getBeanClass获取bean定义的class类型

  该方法默认返回null,但是子类PropertyPlaceholderBeanDefinitionParser重写了该方法,默认返回PropertySourcesPlaceholderConfigurer.class。
  PropertyPlaceholderBeanDefinitionParser就是< context:property-placeholder/>扩展标签的解析器,我们看到实际上它做的事并不多,这就得益于Java的继承体系,很多的事都被它的父类做了。
  实际上后面占位符的解析就是靠这里设置的PropertySourcesPlaceholderConfigurer类的实例来实现的。

/**
 1. PropertyPlaceholderBeanDefinitionParser的方法,返回类型
 2.  3. @param element 标签元素,没用到
 4. @return bean定义的class
 */
@Override
@SuppressWarnings("deprecation")
protected Class<?> getBeanClass(Element element) {
     
    /*
     * 自Spring 3.1 开始,system-properties-mode的属性的默认值从'FALLBACK' 变成 'ENVIRONMENT'
     * 'ENVIRONMENT'表示占位符对系统属性的解析是环境及其当前属性源集的函数。
     */
    if (SYSTEM_PROPERTIES_MODE_DEFAULT.equals(element.getAttribute(SYSTEM_PROPERTIES_MODE_ATTRIBUTE))) {
     
        /*
         * 返回PropertySourcesPlaceholderConfigurer.class,该类自Spring 3.1 开始用于替代PropertyPlaceholderConfigurer
         * 因为该类更加灵活,支持Environment本地环境变量属性源、外部配置属性源、以及Spring 3.1的PropertySource属性源机制
         *
         * 用来解析替换bean定义内的属性值和@Resource、@Value等注解的值中的${..:..}占位符。
         */
        return PropertySourcesPlaceholderConfigurer.class;
    }
    /*
     * 仅仅为了兼容Spring 3.0以及更早的版本,该类实现以及PropertyPlaceholderConfigurer都被标记为废弃
     * 我们不应该继续使用该类,所以现在有些文章或者教程中还在使用这个类是不明智的
     */
    return org.springframework.beans.factory.config.PropertyPlaceholderConfigurer.class;
}

1.1.1.2 doParse解析自有属性

  不同标签的自有属性的解析是通过内部的doParse方法解析的,这个方法由具体的解析器子类实现。
  对于PropertyPlaceholderBeanDefinitionParser,主要就是解析< context:property-placeholder/>扩展标签的一些属性:

  1. ignore-unresolvable:是否忽略不能解析且没有默认值的占位符,默认false,不忽略,将抛出异常,对应PropertyPlaceholderBeanDefinitionParser的ignoreUnresolvablePlaceholders属性;
  2. system-properties-mode:占位符查找规则,默认是ENVIRONMENT,将会首先在environment环境变量属性源中查找,找不到才会在本地配置文件属性源中查找,对应PropertyPlaceholderBeanDefinitionParser的systemPropertiesModeName属性;
  3. value-separator:占位符默认值的分隔符号,默认为 “ : ”,对应PropertyPlaceholderBeanDefinitionParser的valueSeparator属性;
  4. trim-values:是否将解析后的值的前后空白去除,默认不去除,对应PropertyPlaceholderBeanDefinitionParser的trimValues属性;
  5. null-value:空值的定义,默认就是null,对应PropertyPlaceholderBeanDefinitionParser的nullValue属性
  6. location:本地配置文件的路径字符串,支持“ , ”分隔。对应PropertyPlaceholderBeanDefinitionParser的locations属性。在创建实例的时候将会被解析为对应的Resource资源。

  当然还有其他比如“ignore-resource-not-found”属性——用于是否忽略无法解析的本地配置文件路径,默认false,以及“order”——用于指定配置文件加载顺序……等等属性,我们都很少用到。

//-------PropertyPlaceholderBeanDefinitionParser的相关属性-------

private static final String SYSTEM_PROPERTIES_MODE_ATTRIBUTE = "system-properties-mode";

private static final String SYSTEM_PROPERTIES_MODE_DEFAULT = "ENVIRONMENT";

/**
 * PropertyPlaceholderBeanDefinitionParser的方法
 * 

* 解析自有的属性 * * @param element 标签元素 * @param parserContext 解析上下文 * @param builder bean定义构建者 */ @Override protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { //调用父类AbstractPropertyLoadingBeanDefinitionParser的方法 super.doParse(element, parserContext, builder); //解析ignore-unresolvable属性,表示是否忽略没有默认值的无法解析的占位符,默认false,即不能忽略,设置为ignoreUnresolvablePlaceholders属性的值 builder.addPropertyValue("ignoreUnresolvablePlaceholders", Boolean.valueOf(element.getAttribute("ignore-unresolvable"))); //解析system-properties-mode属性,默认ENVIRONMENT String systemPropertiesModeName = element.getAttribute(SYSTEM_PROPERTIES_MODE_ATTRIBUTE); if (StringUtils.hasLength(systemPropertiesModeName) && !systemPropertiesModeName.equals(SYSTEM_PROPERTIES_MODE_DEFAULT)) { builder.addPropertyValue("systemPropertiesModeName", "SYSTEM_PROPERTIES_MODE_" + systemPropertiesModeName); } //解析value-separator属性 if (element.hasAttribute("value-separator")) { builder.addPropertyValue("valueSeparator", element.getAttribute("value-separator")); } //解析trim-values属性 if (element.hasAttribute("trim-values")) { builder.addPropertyValue("trimValues", element.getAttribute("trim-values")); } //解析null-value属性 if (element.hasAttribute("null-value")) { builder.addPropertyValue("nullValue", element.getAttribute("null-value")); } } /** * AbstractPropertyLoadingBeanDefinitionParser的方法 *

* 用于解析之类的扩展标签的属性解析 * * @param element 标签元素 * @param parserContext 解析上下文 * @param builder bean定义构建者 */ @Override protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { //解析location属性 String location = element.getAttribute("location"); if (StringUtils.hasLength(location)) { //属性文件的路径也支持占位符${..},但是只能使用environment中的属性源 location = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(location); //根据","拆分为路径数组 String[] locations = StringUtils.commaDelimitedListToStringArray(location); //设置到propertyValues属性中,在创建实例的时候将会被解析为Resource builder.addPropertyValue("locations", locations); } //解析properties-ref属性 String propertiesRef = element.getAttribute("properties-ref"); if (StringUtils.hasLength(propertiesRef)) { builder.addPropertyReference("properties", propertiesRef); } //解析file-encoding属性 String fileEncoding = element.getAttribute("file-encoding"); if (StringUtils.hasLength(fileEncoding)) { builder.addPropertyValue("fileEncoding", fileEncoding); } //解析order属性 String order = element.getAttribute("order"); if (StringUtils.hasLength(order)) { builder.addPropertyValue("order", Integer.valueOf(order)); } //解析ignoreResourceNotFound属性 builder.addPropertyValue("ignoreResourceNotFound", Boolean.valueOf(element.getAttribute("ignore-resource-not-found"))); //解析localOverride属性 builder.addPropertyValue("localOverride", Boolean.valueOf(element.getAttribute("local-override"))); //设置role属性 builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); }

1.2 parse方法小结

  实际上,我们能发现< context:property-placeholder/>扩展标签被解析、加载完毕时,设置的本地配置文件的中的属性键值对并没有立即被加载进来,并没有真正的被解析。实际上,真正的解析是通过最后注册的PropertySourcesPlaceholderConfigurer类型的bean定义的实例来实现的。
  < context:property-placeholder/>扩展标签被解析、加载完毕之后,beanFactory工厂缓存中仅仅会多了一个bean定义,保存了XML中配置的各种属性值,它的名字id明显是根据XML的规则自动生成的。当然下面我们就会将PropertySourcesPlaceholderConfigurer的实例化
< context:property-placeholder/>标签以及PropertySourcesPlaceholderConfigurer占位符解析器源码深度解析【一万字】_第2张图片

2 PropertySourcesPlaceholderConfigurer工作流程

< context:property-placeholder/>标签以及PropertySourcesPlaceholderConfigurer占位符解析器源码深度解析【一万字】_第3张图片
  PropertySourcesPlaceholderConfigurer是用来解析、替换bean定义内的属性值和@Resource、@Value等依赖注入注解的属性值中的${… : …}占位符的一个工具类。
  在前面解析< context:property-placeholder/>扩展标签时,我们知道注册的bean定义的默认类型为PropertySourcesPlaceholderConfigurer。该类实际上同时属于EnvironmentAware、BeanNameAware、BeanFactoryAware、BeanFactoryPostProcessor类型。由于BeanFactoryPostProcessor的特性,它对应的bean定义将会在普通bean被创建之前实例化,并且它的postProcessBeanFactory和setEnvironment、setBeanName、setBeanFactory等方法会被自动回调,特别是postProcessBeanFactory方法,该方法被回调的时候,才会真正的解析本地属性文件的属性源以及进行占位符的替换。
  在上面标签解析过程中的getBeanClass方法中,我们知道PropertySourcesPlaceholderConfigurer是Spring 3.1以及之后用来替换PropertyPlaceholderConfigurer的工具类,相比于PropertyPlaceholderConfigurer,它的功能更强,支持Environment本地环境变量属性源、外部配置属性源、以及Spring 3.1的PropertySource属性源机制。PropertyPlaceholderConfigurer已被废弃,除非你的Spring版本在3.1之下,一律不推荐使用,Spring 3.1之下的版本还是默认使用PropertyPlaceholderConfigurer,主要是为了兼容性。
  由于PropertySourcesPlaceholderConfigurer属于BeanFactoryPostProcessor类型,那么它将会在refresh方法中的invokeBeanFactoryPostProcessors方法中被初始化并回调postProcessBeanFactory方法。它的仅有一个无参构造器,其中什么也没做,因此我们直接看postProcessBeanFactory方法的源码。

2.1 postProcessBeanFactory加载属性源、解析占位符

  该回调方法非常重要,主要做两件事,第一件事就是配置属性源,第二件事就是使用配置的属性源解析全部bean定义中的占位符:

  1. 如果propertySources属性为null:
    1. 设置environment属性环境变量中的全部属性源成为environmentProperties属性源;
    2. 通过mergeProperties方法将所有本地配置的属性源文件中的属性成为localProperties属性源,也就是< context:property-placeholder/>标签配置的属性文件。
    3. 这两个属性源,environmentProperties在前,localProperties在后,存入propertySources属性源集合中,在后面解析占位符的时候,默认也是先在环境变量属性源中查找,找不到才会在本次配置的属性源中查找。
  2. 如果此前已通过setPropertySources方法设置了自定义的propertySources属性源(默认没有设置),那么上面所有的environment和本地属性源都被将忽略,仅使用自定义的属性源中的属性进行占位符替换。
  3. 调用processProperties方法,访问当前beanFactory 工厂中的每个 bean 定义,并尝试将bean定义内部的${… : …}占位符替换为来自给定propertySources属性源的值。这一步就是解析bean定义中的占位符。
  4. 将appliedPropertySources设置为propertySources的值。
//-------------PropertySourcesPlaceholderConfigurer的相关属性------------

/**
 * 本地属性源名称
 */
public static final String LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME = "localProperties";

/**
 * 环境变量属性源名称
 */
public static final String ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME = "environmentProperties";

/**
 * 存放自定义的属性源
 */
@Nullable
private MutablePropertySources propertySources;

/**
 * 应用属性来源
 */
@Nullable
private PropertySources appliedPropertySources;

/**
 * 环境变量
 */
@Nullable
private Environment environment;


/**
 * 由于PropertySourcesPlaceholderConfigurer的属于EnvironmentAware,那么该方法将会在实例化该类时,被自动回调
 * 实际上就是ApplicationContextAwareProcessor的postProcessBeforeInitialization方法设置的,由initializeBean方法调用
 */
@Override
public void setEnvironment(Environment environment) {
     
    this.environment = environment;
}


/**
 * PropertySourcesPlaceholderConfigurer的方法
 * 

* 配置后续用于替换${...}占位符的属性源到PropertySources中,属性源来自: * 1 所有的Environment环境变量中的属性源,比如systemEnvironment和systemProperties,setEnvironment * 2 本地配置的属性文件引入的属性源头,mergeProperties、setLocation、setLocations、setProperties * 3 通过PropertySource引入的属性源文件,setPropertySources *

* 如果setPropertySources方法已被调用并设置了值,那么所有的environment和本地属性源都被将忽略 */ @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { /* * 如果自定义的属性源为null,那么根据environment和本地配置属性构建属性源,默认就是null */ if (this.propertySources == null) { //新建可变的属性源集合,内部可以出有多个属性源 this.propertySources = new MutablePropertySources(); //如果environment不为null if (this.environment != null) { //根据name=environmentProperties以及environment,构建一个PropertySource属性源并加入到propertySources集合尾部 this.propertySources.addLast( new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) { //内部的getProperty方法我们在Spring源码的第一篇文件就讲过了 @Override @Nullable public String getProperty(String key) { return this.source.getProperty(key); } } ); } try { //根据name=localProperties以及通过mergeProperties方法加载的所有本地配置的属性源,构建一个PropertiesPropertySource属性源 PropertySource<?> localPropertySource = new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties()); //默认false if (this.localOverride) { this.propertySources.addFirst(localPropertySource); } else { //将加载的所有本地配置的属性源加入到propertySources集合尾部 this.propertySources.addLast(localPropertySource); } } catch (IOException ex) { throw new BeanInitializationException("Could not load properties", ex); } } /* * 根据当前的propertySources新建一个PropertySourcesPropertyResolver * 调用processProperties方法,访问给定 bean 工厂中的每个 bean 定义,并尝试将内部的${...}占位符替换为来自给定propertySources属性源的值 */ processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources)); //设置appliedPropertySources属性 this.appliedPropertySources = this.propertySources; }

2.1.1 mergeProperties合并本地属性源

  mergeProperties方法用于返回包含此工厂中设置的外部属性文件的属性合并之后的属性集合实例,也就是将我们配置的所有属性文件中的属性合并成为一个Properties集合并返回,由于这是一个map,很明显同名的属性的值会相互覆盖。
  这个方法是位于PropertySourcesPlaceholderConfigurer的父类PropertiesLoaderSupport中的方法,从类名可以看出来,该类专门用于加载外部属性。
  我们在< context:property-placeholder/>中配置的属性源文件,它将会在创建PropertySourcesPlaceholderConfigurer实例的时候,会自动加载成为Resource资源,这里的方法就是继续解析Resource资源获取内部的属性键值对。

//-----------PropertiesLoaderSupport的相关属性----------

/**
 * 本地属性源覆盖,默认false
 */
protected boolean localOverride = false;
/**
 * 手动设置的本地属性
 */
@Nullable
protected Properties[] localProperties;

/**
 * PropertiesLoaderSupport的方法
 * 

* 返回包含此工厂中设置的外部属性文件的属性合并之后的属性集合实例 */ protected Properties mergeProperties() throws IOException { //合并属性集合 Properties result = new Properties(); //localOverride属性默认false,因此不会覆盖 if (this.localOverride) { // Load properties from file upfront, to let local properties override. loadProperties(result); } //如果本地属性不为null if (this.localProperties != null) { //那么合并属性,因此相同key的属性将会覆盖 for (Properties localProp : this.localProperties) { CollectionUtils.mergePropertiesIntoMap(localProp, result); } } //加载本地配置的属性文件中的属性 if (!this.localOverride) { //从本地文件文件加载属性,以允许这些属性重写。 loadProperties(result); } return result; } /** * PropertiesLoaderSupport的属性 * 本地文件对应的的Resource资源 * 在创建PropertyPlaceholderBeanDefinitionParser实例时,将会把每一个路径解析为Resource资源 */ @Nullable private Resource[] locations; /** 1. PropertiesLoaderSupport的方法 2.

3. 将本地文件中的属性加载到给定的实例中。 4. 5. @param props 要加载到的属性集合实例 */ protected void loadProperties(Properties props) throws IOException { //遍历加载进来的文件Resource资源 if (this.locations != null) { for (Resource location : this.locations) { if (logger.isTraceEnabled()) { logger.trace("Loading properties file from " + location); } try { //将Resource资源中的属性键值对加载到给定的props集合中 PropertiesLoaderUtils.fillProperties( props, new EncodedResource(location, this.fileEncoding), this.propertiesPersister); } catch (FileNotFoundException | UnknownHostException ex) { if (this.ignoreResourceNotFound) { if (logger.isDebugEnabled()) { logger.debug("Properties resource not found: " + ex.getMessage()); } } else { throw ex; } } } } }

2.1.2 processProperties替换占位符

  processProperties方法用于访问给定 bean 工厂中的每个 bean 定义,并尝试将内部的${… : …}占位符通过propertyResolver替换为来自给定propertySources属性源的值。

  1. 设置占位符的语法格式,默认就是${… : …}。 我们可以通过自定义一个PropertySourcesPlaceholderConfigurer来自定义占位符格式。
  2. 设置一个StringValueResolver类型的解析器,这是一个lambda对象,后续就是通过调用该解析器对象的resolveStringValue方法来解析占位符的,该方法实际上还是根据是否允许忽略无法解析且没有默认值的占位符调用propertyResolver参数对象propertyResolver的resolvePlaceholders或者resolveRequiredPlaceholders方法,这两个方法我们在容器初始化源码的第一部分setConfigLocations中有详细讲解。如果使用< context:property-placeholder/>标签,那么ignoreUnresolvablePlaceholders对应着的ignore-unresolvable属性,默认都是false,即不忽略。
  3. 调用父类PlaceholderConfigurerSupport的方法,检查当前bean工厂中的所有bean定义,使用新建的StringValueResolver解析器替换占位符。
//---------父类PlaceholderConfigurerSupport的相关属性

/*
 * 占位符的相关格式语法,我们可以自定义占位符的格式,默认就是${.. : ..}
 * 通过重写setPlaceholderPrefix、setPlaceholderSuffix、setValueSeparator方法
 */

/**
 * Default placeholder prefix: {@value}.
 */
public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";

/**
 * Default placeholder suffix: {@value}.
 */
public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";

/**
 * Default value separator: {@value}.
 */
public static final String DEFAULT_VALUE_SEPARATOR = ":";


/**
 * Defaults to {@value #DEFAULT_PLACEHOLDER_PREFIX}.
 */
protected String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX;

/**
 * Defaults to {@value #DEFAULT_PLACEHOLDER_SUFFIX}.
 */
protected String placeholderSuffix = DEFAULT_PLACEHOLDER_SUFFIX;

/**
 * Defaults to {@value #DEFAULT_VALUE_SEPARATOR}.
 */
@Nullable
protected String valueSeparator = DEFAULT_VALUE_SEPARATOR;

/**
 * 是否允许忽略无法解析的没有默认值的占位符
 * 对应着的ignore-unresolvable属性,默认都是false
 */
protected boolean ignoreUnresolvablePlaceholders = false;

/**
 * 在进行占位符解析之前是否去除原始值的前后空白字符
 */
protected boolean trimValues = false;

/**
 * 默认null
 */
@Nullable
protected String nullValue;

/**
 1. PropertySourcesPlaceholderConfigurer的方法
 2. 

3. 访问给定 bean 工厂中的每个 bean 定义,并尝试将内部的${...}占位符替换为来自给定属性源的值。 4. 5. @param beanFactoryToProcess 需要处理的beanFactory 6. @param propertyResolver 可配置属性解析器 */ protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, final ConfigurablePropertyResolver propertyResolver) throws BeansException { //设置占位符的格式,可以自定义 propertyResolver.setPlaceholderPrefix(this.placeholderPrefix); propertyResolver.setPlaceholderSuffix(this.placeholderSuffix); propertyResolver.setValueSeparator(this.valueSeparator); /* * 创建StringValueResolver的lambda对象,它的resolveStringValue方法实际上就是调用propertyResolver的方法 * 如果允许忽略无法解析的没有默认值的占位符,那么调用resolvePlaceholders方法,否则调用resolveRequiredPlaceholders方法 * 这两个方法我们在IoC容器第一部分setConfigLocations部分的源码中都讲过了 * * 后续就是通过调用该解析器对戏那个来解析占位符的 */ StringValueResolver valueResolver = strVal -> { String resolved = (this.ignoreUnresolvablePlaceholders ? propertyResolver.resolvePlaceholders(strVal) : propertyResolver.resolveRequiredPlaceholders(strVal)); //在返回解析结果在前是否应该去除前后空白字符,对应着的trim-values属性 //没有XML的默认值,属性默认为false if (this.trimValues) { resolved = resolved.trim(); } //属性值是否为nullValue,如果是那么返回null,否则直接返回解析后的值 return (resolved.equals(this.nullValue) ? null : resolved); }; /* * 调用父类PlaceholderConfigurerSupport的方法,检查所有bean定义,替换占位符 */ doProcessProperties(beanFactoryToProcess, valueResolver); }

2.1.2.1 doProcessProperties替换占位符

  该方法是真正的替换占位符的方法,大概步骤为:

  1. 构建一个BeanDefinitionVisitor访问者,用于遍历BeanDefinition对象的属性,在遍历的同时替换值中的占位符。
  2. 排除当前PropertySourcesPlaceholderConfigurer的bean定义和其他beanFactory中的bean定义,遍历当前beanFactory所有已注册的bean定义对象,调用visitBeanDefinition方法对它们的属性进行占位符的替换,支持占位符的地方有:
    1. < bean/>的标签的parent、class、factory-bean、factory-method、scope属性。
    2. < property/>标签的value、< constructor-arg/>标签的value,这两个value可以是各种类型,比如BeanDefinition、List。
    3. 对于注解配置的属性,也会进行占位符解析,比如@Scope。
  3. Spring 2.5 中的新功能:也解析别名映射缓存aliasMap中的 value-目标名称和key-别名 中的占位符。
  4. Spring 3.0 中的新功能:将当前的valueResolver加入embeddedValueResolvers中,用于后续比如@Value、@Resource等依赖注入注解中的占位符解析,注意这里并没有立即解析,因为invokeBeanFactoryPostProcessors方法调用的时候,bean的相关依赖注入的注解还没有被解析,在后面的finishBeanFactoryInitialization方法实例化bean的时候才会用到。
/*
 * PlaceholderConfigurerSupport的相关属性
 * 

* PlaceholderConfigurerSupport属于BeanNameAware和BeanFactoryAware的实例 * 因此在创建PropertySourcesPlaceholderConfigurer实例时会自动回调setBeanName和setBeanFactory方法 */ @Nullable private String beanName; @Nullable private BeanFactory beanFactory; /** * 创建PropertySourcesPlaceholderConfigurer实例时回调 * * @param beanName 当前实例beanName */ @Override public void setBeanName(String beanName) { this.beanName = beanName; } /** * 创建PropertySourcesPlaceholderConfigurer实例时回调 * * @param beanFactory 当前工厂 */ @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } /** 1. PlaceholderConfigurerSupport的属性 2.

3. 检查所有bean定义,替换占位符 4. 5. @param beanFactoryToProcess 需要处理的beanFactory 6. @param valueResolver 值解析器 */ protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess, StringValueResolver valueResolver) { /* * 创建一个用于遍历BeanDefinition对象的访问者类,可以访问给定bean定义的属性 * 特别是其中包含的属性值和构造函数参数值,在遍历的同时解析值中的占位符 */ BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver); //获取所有已注册的beanName数组 String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames(); /* * 1 遍历beanName数组,获取bean定义,解析替换内部属性值中的占位符 */ for (String curName : beanNames) { // Check that we're not parsing our own bean definition, // to avoid failing on unresolvable placeholders in properties file locations. //如果当前遍历的beanName不等于当前注册的PropertySourcesPlaceholderConfigurer的beanName //并且是同一个beanFactory,那么可以解析其中的占位符 if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) { //获取bena定义 BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName); try { /* * 遍历给定的 Bean 定义对象属性,包括MutablePropertyValues属性值和ConstructorArgumentValues构造器值 * 对遍历的每一个值使用valueResolver替换其中的占位符 * 支持占位符的属性有: * 1 的标签的parent属性、class属性、factory-bean属性、factory-method属性 * 2 标签的value、标签的value */ visitor.visitBeanDefinition(bd); } catch (Exception ex) { throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex); } } } /* * 2 Spring 2.5 中的新功能:也解析别名映射缓存aliasMap中的 value-目标名称和key-别名 中的占位符。 */ beanFactoryToProcess.resolveAliases(valueResolver); /* * 3 Spring 3.0 中的新功能:将当前的valueResolver加入embeddedValueResolvers中,用于后续比如@Value、@Resource注解中的占位符解析, * 注意这里并没有立即解析,因为invokeBeanFactoryPostProcessors方法调用的时候,bean的相关注解还没有被解析, * 在后面的finishBeanFactoryInitialization方法中才会用到 */ beanFactoryToProcess.addEmbeddedValueResolver(valueResolver); }

2.2 postProcessBeanFactory方法小结

  postProcessBeanFactory方法的执行流程就是PropertySourcesPlaceholderConfigurer的工作流程之一!

  1. 默认使用属性源包括environment环境变量、和本地属性配置文件。替换占位符的时候,优先去environment中的查找。
  2. 会对bean定义中的某些占位符进行解析替换,常见的就是XML配置的bean定义中的占位符,支持的占位符的有:
    1. < bean/>的标签的parent、class、factory-bean、factory-method、scope属性。
    2. < property/>标签的value、< constructor-arg/>标签的value,这两个value可以是各种类型,比如BeanDefinition、List。
    3. 对于注解配置的属性,也会进行占位符解析,比如@Scope。
  3. 会对别名映射缓存aliasMap中的 value-目标名称和key-别名 中的占位符进行替换。 这是Spring 2.5的新功能。
  4. 会将当前的解析器缓存起来,后面的finishBeanFactoryInitialization方法实例化bean的时候,在后续对依赖注入注解比如@Value、@Resource进行解析的时候还会用,即同样会使用配置的本地属性文件中的属性,进行占位符的替换。 这是Spring 3.0的新功能。

3 多个< context:property-placeholder/>配置

  从源码中,我们能够看出来,每一个< context:property-placeholder/>扩展标签,在解析后将会对应着一个PropertySourcesPlaceholderConfigurer类型的bean定义,这些bean定义由于是BeanFactoryPostProcessor类型,因此都会在invokeBeanFactoryPostProcessors方法中被初始化并且有顺序的回调相关的postProcessBeanFactory方法。所以,实际上PropertySourcesPlaceholderConfigurer对象可以存在多个,< context:property-placeholder/>标签也可以指定多个。
  因此,许多其他文章认为的只能有一个< context:property-placeholder/>标签的配置的说法完全是错误的。但是,这些文章还举例“证明了”它们的观点,如果同时定义多个< context:property-placeholder/>,那么在解析的占位符的时候就会抛出异常,而我们在测试的时候,好像发现确实如此,但这明显是“用一些事实来证明自己错误的观点”,就算是给出的原因也是五花八门,比如“只能有一个PropertySourcesPlaceholderConfigurer,多个就会造成相互覆盖”等等,这些都是不可信的,这就是典型的没看源码的后果(或许Spring之前的版本是这样的?)。那么,抛出异常的真正原因是什么呢?
  如果你阅读了此前本人写的IoC容器初始化源码(最主要是本文的源码),就会知道,PropertySourcesPlaceholderConfigurer 对于无法解析并且没有默认值的占位符的情况,默认情况下直接抛出异常,说到这你可能就明白了:有可能某些占位符属性被设置在第二个PropertySourcesPlaceholderConfigurer属性源中,当第一个PropertySourcesPlaceholderConfigurer在解析占位符时由于没在它内部的属性源中找到该占位符对应的属性,因此默认抛出异常。但是实际上这个占位符是能够被解析的,只不过对应的属性是被设置在第二个PropertySourcesPlaceholderConfigurer属性源中,怎么办呢?很简单,那就是设置标签的ignore-unresolvable属性为true,表示忽略无法解析且没有默认值的占位符,这样,该占位符在第一个PropertySourcesPlaceholderConfigurer中无法解析时就不会抛出异常,从而能够正常的被第二个PropertySourcesPlaceholderConfigurer解析。
  而一般情况下,我们可以通过一个< context:property-placeholder/>引入多个配置文件,这样就不存在该问题了。

3.1 测试案例

  两个properties配置文件:
  ph.properties,有一个属性:

config=ph

  ph2.properties,什么属性都没有。
  测试类,com.spring.source.propertyPlaceholder.PHTest:

@Component
public class PHTest {
     

    @Resource(name = "${config}")
    private PH ph2;

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

    @Override
    public String toString() {
     
        return "PHTest{" +
                "ph2=" + ph2 +
                ", ph3='" + ph3 + '\'' +
                '}';
    }

    @Component("ph")
    public static class PH {
     

    }
}

  配置文件,spring-config-ph.xml:

<context:property-placeholder location="ph.properties"/>
<context:property-placeholder location="ph2.properties"/>

<context:component-scan base-package="com.spring.source.propertyPlaceholder"/>

  我们看到,设置了两个< context:property-placeholder/>标签,但是ph.properties配置在前面,那么我们测试一下:

@Test
public void ph() {
     
    ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config-ph.xml");
    System.out.println(ac.getBean("PHTest"));
}

  结果如下,成功的注入,即成功的解析了占位符:

PHTest{
     ph2=com.spring.source.propertyPlaceholder.PHTest$PH@47542153, ph3='ph'}

  那么,我们将这两个标签换个位置:

<context:property-placeholder location="ph2.properties"/>
<context:property-placeholder location="ph.properties"/>

<context:component-scan base-package="com.spring.source.propertyPlaceholder"/>

  很明显,第一个标签先被解析,但是它的PropertySourcesPlaceholderConfigurer内部并没有${config}占位符所需要的属性,所以,直接抛出异常。

Could not resolve placeholder 'config' in value "${config}"

  但是这个属性在第二个PropertySourcesPlaceholderConfigurer的属性源中更的确是存在的,debug就能直观的看出来,我们在AbstractBeanFactory. resolveEmbeddedValue方法中打上断点(该方法用于解析@Resource注解的占位符):
< context:property-placeholder/>标签以及PropertySourcesPlaceholderConfigurer占位符解析器源码深度解析【一万字】_第4张图片
  可以明显看到两个PropertySourcesPlaceholderConfigurer内部的lambda对象,随后我们在PropertySourcesPlaceholderConfigurer的processProperties方法处打上断点:
< context:property-placeholder/>标签以及PropertySourcesPlaceholderConfigurer占位符解析器源码深度解析【一万字】_第5张图片
  可以看到,内部的本地属性源并没有任何属性(因为ph2.properties是一个空文件),因此该占位符无法成功解析,又因为标签默认ignore-unresolvable属性默认为false,因此在解析过程中抛出异常。
  所以,我们将ignore-unresolvable都设置为true,表示忽略当前无法解析并且没有默认值的占位符就行了:

<context:property-placeholder location="ph2.properties" ignore-unresolvable="true"/>
<context:property-placeholder location="ph.properties" ignore-unresolvable="true"/>

<context:component-scan base-package="com.spring.source.propertyPlaceholder"/>

  再次测试,成功解析:

PHTest{
     ph2=com.spring.source.propertyPlaceholder.PHTest$PH@47542153, ph3='ph'}

4 PropertySourcesPlaceholderConfigurer总结

  PropertySourcesPlaceholderConfigurer的postProcessBeanFactory方法首先会加载environment和当前bean定义中的locations属性设置的本地配置文件这两处的属性源到该对象自身的属性源集合propertySources中(这个属性是对象级别的,不会共享),随后调用processProperties方法,初始化属于自己的StringValueResolver解析器,遍历全部bean定义,对其中的某些值进行占位符的替换,Spring 2.5开始还支持别名映射缓存中占位符的替换,Spring 3.0开始还支持对后续@Value、@Resource等依赖注入注解解析的值中的占位符的替换。
  注意这里的processProperties方法中并没有立即对@Value、@Resource等注解的属性值中的占位符替换,而是将每一个PropertySourcesPlaceholderConfigurer对象自己的解析器和自己内部的属性源统一存入beanFactory的一个embeddedValueResolver集合类型的属性中,在finishBeanFactoryInitialization部分真正解析这些注解进行依赖注入的时候,才会遍历embeddedValueResolver集合中的解析器,进行占位符的替换,我们在IoC容器初始化的第5、6、7篇文章中有介绍。
  我们只需要记住:PropertySourcesPlaceholderConfigurer就是用来解析、替换bean定义内的属性值和@Resource、@Value等依赖注入注解的属性值中的${… : …}占位符的一个工具类,内部使用了environment和本地配置文件两个属性源而其他地方的占位符的解析是不会在本地配置文件属性源中查找的,比如< context:property-placeholder/>的location属性、< context:component-scan/>标签的base-package属性、< import/>标签的resource属性……等等属性中的占位符,因为它们的占位符解析时,PropertySourcesPlaceholderConfigurer,即本地配置文件属性源根本就还没有加载,只会从environment环境变量的属性源中查找属性,因此,一定要注意这个细节。
  实际上, Spring 3.1开始还支持@PropertySource、@PropertySources注解形式的属性源引入配置,这个注解是被ConfigurationClassPostProcessor后处理器解析的,ConfigurationClassPostProcessor是另一个专门处理某些配置注解的核心后处理器。并且,有意思的是,如果通过@PropertySource注解引入本地配置文件中的属性源,那么这个属性源被存入environment环境变量中,如果只有通过该注解引入属性源,那么不会初始化任何PropertySourcesPlaceholderConfigurer,这样导致的一个现象就是:对于无法解析并且没有默认值的占位符,默认将会直接使用占位符本身作为注入值而不是抛出异常,这是由于在finishBeanFactoryInitialization中判断没有注册任何嵌入值后处理器比如PropertySourcesPlaceholderConfigurer,那么会注册一个默认的嵌入值解析器lambda对象,将会使用environment环境变量对象的属性源,以及使用resolvePlaceholders方法来作为resolveStringValue方法的方法体,忽略没有默认值且无法解析的占位符(IoC容器初始化5的文章源码有讲解)。实际上boot应用的配置文件的属性也是被存入environment环境变量中的,但是boot应用会自动的帮我们注入一个PropertySourcesPlaceholderConfigurer,因此单纯使用注解也不会抛出异常。这些知识点,我们后面有空再讲解。

相关文章:
  https://spring.io/
  Spring 5.x 学习
  Spring 5.x 源码

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

你可能感兴趣的:(#,Spring,5.x,源码,Spring,源码,Spring占位符解析)