2022-08-23  阅读(42)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/series/article/1805029960

在上文 【死磕 Spring】----- IOC 之 深入分析 BeanFactoryPostProcessor 介绍了 BeanFactoryPostProcessor,知道 BeanFactoryPostProcessor 作用域容器启动阶段,可以对解析好的 BeanDefinition 进行定制化处理,而其中 PropertyPlaceholderConfigurer 是其一个非常重要的应用,也是其子类,介绍如下:

PropertyPlaceholderConfigurer 允许我们用 Properties 文件中的属性来定义应用上下文(配置文件或者注解)

什么意思,就是说我们在 XML 配置文件(或者其他方式,如注解方式)中使用占位符的方式来定义一些资源,并将这些占位符所代表的资源配置到 Properties 中,这样只需要对 Properties 文件进行修改即可,这个特性非常,在后面来介绍一种我们在项目中经常用到场景。

202202131351052621.png

从 PropertyPlaceholderConfigurer 的结构图可以看出,它间接实现了 Aware 和 BeanFactoryPostProcessor 两大扩展接口,这里只需要关注 BeanFactoryPostProcessor 即可。我们知道 BeanFactoryPostProcessor 提供了 postProcessBeanFactory(),在这个体系中该方法的是在 PropertyResourceConfigurer 中实现,该类为属性资源的配置类,他实现了 BeanFactoryPostProcessor 接口,如下:

             public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
              try {
               Properties mergedProps = mergeProperties();
            
               // 转换合并属性
               convertProperties(mergedProps);
            
               // 子类处理
               processProperties(beanFactory, mergedProps);
              }
              catch (IOException ex) {
               throw new BeanInitializationException("Could not load properties", ex);
              }
             }
  • mergeProperties():返回合并的 Properties 实例,Properties 实例维护这一组 key-value ,其实就是 Properties 配置文件中的内容。
  • convertProperties():转换合并的值,其实就是将原始值替换为真正的值
  • processProperties():前面两个步骤已经将配置文件中的值进行了处理,那么该方法就是真正的替换过程,该方法由子类实现。

在 PropertyPlaceholderConfigurer 重写 processProperties():

             protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props)
               throws BeansException {
            
              StringValueResolver valueResolver = new PlaceholderResolvingStringValueResolver(props);
              doProcessProperties(beanFactoryToProcess, valueResolver);
             }

首先构造一个 PlaceholderResolvingStringValueResolver 类型的 StringValueResolver 实例。StringValueResolver 为一个解析 String 类型值的策略接口,该接口提供了 resolveStringValue() 方法用于解析 String 值。PlaceholderResolvingStringValueResolver 为其一个解析策略,构造方法如下:

            public PlaceholderResolvingStringValueResolver(Properties props) {
             this.helper = new PropertyPlaceholderHelper(
               placeholderPrefix, placeholderSuffix, valueSeparator, ignoreUnresolvablePlaceholders);
             this.resolver = new PropertyPlaceholderConfigurerResolver(props);
            }

在构造 String 值解析器 StringValueResolver 时,将已经解析的 Properties 实例对象封装在 PlaceholderResolver 实例 resolver 中。PlaceholderResolver 是一个用于解析字符串中包含占位符的替换值的策略接口,该接口有一个 resolvePlaceholder() 方法,用于返回占位符的替换值。还有一个 PropertyPlaceholderHelper 工具,从名字上面看应该是进行替换的。

得到 String 解析器的实例 valueResolver 后,则会调用 doProcessProperties() 方法来进行诊治的替换操作,该方法在父类 PlaceholderConfigurerSupport 中实现,如下:

             protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
               StringValueResolver valueResolver) {
            
              BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
            
              String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
              for (String curName : beanNames) {
               // 校验
               // 1. 当前实例 PlaceholderConfigurerSupport 不在解析范围内
                  // 2. 同一个 Spring 容器
               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);
                }
               }
              }
            
              // 别名的占位符
              beanFactoryToProcess.resolveAliases(valueResolver);
            
              // 解析嵌入值的占位符,例如注释属性
                    beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
             }

流程如下:

  1. 根据 String 值解析策略 valueResolver 得到 BeanDefinitionVisitor 实例。BeanDefinitionVisitor 是 BeanDefinition 的访问者,我们通过它可以实现对 BeanDefinition 内容的进行访问,内容很多,例如Scope、PropertyValues、FactoryMethodName 等等。
  2. 得到该容器的所有 BeanName,然后对其进行访问(visitBeanDefinition())。
  3. 解析别名的占位符
  4. 解析嵌入值的占位符,例如注释属性

这个方法核心在于 visitBeanDefinition() 的调用,如下:

             public void visitBeanDefinition(BeanDefinition beanDefinition) {
              visitParentName(beanDefinition);
              visitBeanClassName(beanDefinition);
              visitFactoryBeanName(beanDefinition);
              visitFactoryMethodName(beanDefinition);
              visitScope(beanDefinition);
              if (beanDefinition.hasPropertyValues()) {
               visitPropertyValues(beanDefinition.getPropertyValues());
              }
              if (beanDefinition.hasConstructorArgumentValues()) {
               ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
               visitIndexedArgumentValues(cas.getIndexedArgumentValues());
               visitGenericArgumentValues(cas.getGenericArgumentValues());
              }
             }

我们可以看到该方法基本访问了 BeanDefinition 中所有值得访问的东西了,包括 parent 、class 、factory-bean 、factory-method 、scope 、property 、constructor-arg ,本篇文章的主题是 property,所以关注 visitPropertyValues() 即可。如下:

             protected void visitPropertyValues(MutablePropertyValues pvs) {
              PropertyValue[] pvArray = pvs.getPropertyValues();
              for (PropertyValue pv : pvArray) {
               Object newVal = resolveValue(pv.getValue());
               if (!ObjectUtils.nullSafeEquals(newVal, pv.getValue())) {
                pvs.add(pv.getName(), newVal);
               }
              }
             }

过程就是对属性数组进行遍历,调用 resolveValue() 对属性进行解析获取最新值,如果新值和旧值不等,则用新值替换旧值。resolveValue() 实现如下:

             protected Object resolveValue(@Nullable Object value) {
              // 由于 Properties 中的是 String,所以把前面一堆 if 去掉
              else if (value instanceof String) {
               return resolveStringValue((String) value);
              }
              return value;
             }

由于配置的是 String 类型,所以只需要看 String 相关的,resolveStringValue() 实现如下:

              protected String resolveStringValue(String strVal) {
              if (this.valueResolver == null) {
               throw new IllegalStateException("No StringValueResolver specified - pass a resolver " +
                 "object into the constructor or override the 'resolveStringValue' method");
              }
              String resolvedValue = this.valueResolver.resolveStringValue(strVal);
              return (strVal.equals(resolvedValue) ? strVal : resolvedValue);
             }

valueResolver 是我们在构造 BeanDefinitionVisitor 实例时传入的 String 类型解析器 PlaceholderResolvingStringValueResolver,调用其 resolveStringValue() 如下:

            public String resolveStringValue(String strVal) throws BeansException {
             String resolved = this.helper.replacePlaceholders(strVal, this.resolver);
             if (trimValues) {
              resolved = resolved.trim();
             }
             return (resolved.equals(nullValue) ? null : resolved);
            }

helper 为 PropertyPlaceholderHelper 实例对象,而 PropertyPlaceholderHelper 则是处理应用程序中包含占位符的字符串工具类。在构造 helper 实例对象时需要传入了几个参数:placeholderPrefix、placeholderSuffix、valueSeparator,这些值在 PlaceholderConfigurerSupport 中定义如下:

            protected String placeholderPrefix = "${";
            protected String placeholderSuffix = "}";
            protected String valueSeparator = ":";

调用 replacePlaceholders() 进行占位符替换,如下:

             public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
              Assert.notNull(value, "'value' must not be null");
              return parseStringValue(value, placeholderResolver, new HashSet<>());
             }

调用 parseStringValue(),这个方法是这篇博客最核心的地方,${} 占位符的替换:

                protected String parseStringValue(
                        String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
            
                    StringBuilder result = new StringBuilder(value);
            
                    // 获取前缀 "${" 的索引位置
                    int startIndex = value.indexOf(this.placeholderPrefix);
                    while (startIndex != -1) {
            
                        // 获取 后缀 "}" 的索引位置
                        int endIndex = findPlaceholderEndIndex(result, startIndex);
                        if (endIndex != -1) {
            
                            // 截取 "${" 和 "}" 中间的内容,这也就是我们在配置文件中对应的值
                            String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
                            String originalPlaceholder = placeholder;
                            if (!visitedPlaceholders.add(originalPlaceholder)) {
                                throw new IllegalArgumentException(
                                        "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
                            }
                            // 解析占位符键中包含的占位符,真正的值
                            placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
                            // 从 Properties 中获取 placeHolder 对应的值 propVal
                            String propVal = placeholderResolver.resolvePlaceholder(placeholder);
                            // 如果不存在
                            if (propVal == null && this.valueSeparator != null) {
                                // 查询 : 的位置
                                int separatorIndex = placeholder.indexOf(this.valueSeparator);
                                // 如果存在 :
                                if (separatorIndex != -1) {
                                    // 获取 : 前面部分 actualPlaceholder
                                    String actualPlaceholder = placeholder.substring(0, separatorIndex);
                                    // 获取 : 后面部分 defaultValue
                                    String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
            
                                    // 从 Properties 中获取 actualPlaceholder 对应的值
                                    propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
                                    // 如果不存在 则返回 defaultValue
                                    if (propVal == null) {
                                        propVal = defaultValue;
                                    }
                                }
                            }
                            if (propVal != null) {
                                propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
                                result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
                                if (logger.isTraceEnabled()) {
                                    logger.trace("Resolved placeholder '" + placeholder + "'");
                                }
                                startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
                            }
                            else if (this.ignoreUnresolvablePlaceholders) {
                                // 忽略值
                                startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
                            }
                            else {
                                throw new IllegalArgumentException("Could not resolve placeholder '" +
                                        placeholder + "'" + " in value \"" + value + "\"");
                            }
            
                            //
                            visitedPlaceholders.remove(originalPlaceholder);
                        }
                        else {
                            startIndex = -1;
                        }
                    }
            
                    // 返回propVal,就是替换之后的值
                    return result.toString();
                }

流程如下

  1. 获取占位符前缀 "${" 的索引位置 startIndex
  2. 如果前缀 "${" 存在,则从 “{” 后面开始获取占位符后缀 "}" 的索引位置 endIndex
  3. 如果前缀 “${” 和后缀 "}" 都存在,则截取中间部分 placeholder
  4. 从 Properties 中获取 placeHolder 对应的值 propVal
  5. 如果 propVal 为空,则判断占位符中是否存在 ":",如果存在则对占位符进行分割处理,全面部分为 actualPlaceholder,后面部分 defaultValue,尝试从 Properties 中获取 actualPlaceholder 对应的值 propVal,如果不存在,则将 defaultValue 的值赋值给 propVal
  6. 返回 propVal,也就是 Properties 中对应的值

到这里占位符的解析就结束了,下篇我们将利用 PropertyPlaceholderConfigurer 来实现动态加载配置文件,这个场景也是非常常见的。


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文