SpringBoot环境属性占位符解析和类型转换方式

 更新时间:2023年11月27日 11:12:18   作者:Throwable文摘  
这篇文章主要介绍了SpringBoot环境属性占位符解析和类型转换方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

前提

前面写过一篇关于Environment属性加载的源码分析和扩展,里面提到属性的占位符解析和类型转换是相对复杂的,这篇文章就是要分析和解读这两个复杂的问题。

关于这两个问题,选用一个比较复杂的参数处理方法PropertySourcesPropertyResolver#getProperty,解析占位符的时候依赖到PropertySourcesPropertyResolver#getPropertyAsRawString

protected String getPropertyAsRawString(String key) {
    return getProperty(key, String.class, false);
}

protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
    if (this.propertySources != null) {
        for (PropertySource<?> propertySource : this.propertySources) {
            if (logger.isTraceEnabled()) {
                logger.trace("Searching for key '" + key + "' in PropertySource '" +
                            propertySource.getName() + "'");
            }
            Object value = propertySource.getProperty(key);
            if (value != null) {
                if (resolveNestedPlaceholders && value instanceof String) {
                    //解析带有占位符的属性
                    value = resolveNestedPlaceholders((String) value);
                }
                logKeyFound(key, propertySource, value);
                //需要时转换属性的类型
                return convertValueIfNecessary(value, targetValueType);
            }
        }
    }
    if (logger.isDebugEnabled()) {
        logger.debug("Could not find key '" + key + "' in any property source");
    }
    return null;
}

属性占位符解析

属性占位符的解析方法是PropertySourcesPropertyResolver的父类AbstractPropertyResolver#resolveNestedPlaceholders

protected String resolveNestedPlaceholders(String value) {
    return (this.ignoreUnresolvableNestedPlaceholders ?
        resolvePlaceholders(value) : resolveRequiredPlaceholders(value));
}

ignoreUnresolvableNestedPlaceholders属性默认为false,可以通过AbstractEnvironment#setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders)设置,当此属性被设置为true,解析属性占位符失败的时候(并且没有为占位符配置默认值)不会抛出异常,返回属性原样字符串,否则会抛出IllegalArgumentException。

我们这里只需要分析AbstractPropertyResolver#resolveRequiredPlaceholders

//AbstractPropertyResolver中的属性:
//ignoreUnresolvableNestedPlaceholders=true情况下创建的PropertyPlaceholderHelper实例
@Nullable
private PropertyPlaceholderHelper nonStrictHelper;

//ignoreUnresolvableNestedPlaceholders=false情况下创建的PropertyPlaceholderHelper实例
@Nullable
private PropertyPlaceholderHelper strictHelper;

//是否忽略无法处理的属性占位符,这里是false,也就是遇到无法处理的属性占位符且没有默认值则抛出异常
private boolean ignoreUnresolvableNestedPlaceholders = false;

//属性占位符前缀,这里是"${"
private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX;

//属性占位符后缀,这里是"}"
private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX;

//属性占位符解析失败的时候配置默认值的分隔符,这里是":"
@Nullable
private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;


public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
    if (this.strictHelper == null) {
        this.strictHelper = createPlaceholderHelper(false);
    }
    return doResolvePlaceholders(text, this.strictHelper);
}

//创建一个新的PropertyPlaceholderHelper实例,这里ignoreUnresolvablePlaceholders为false
private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
    return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix, this.valueSeparator, ignoreUnresolvablePlaceholders);
}

//这里最终的解析工作委托到PropertyPlaceholderHelper#replacePlaceholders完成
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
    return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}

最终只需要分析PropertyPlaceholderHelper#replacePlaceholders,这里需要重点注意:

- 注意到这里的第一个参数text就是属性值的源字符串,例如我们需要处理的属性为myProperties: server.port−server.port− {spring.application.name},这里的text就是 server\.port−server\.port

− {spring.application.name}。

- replacePlaceholders方法的第二个参数placeholderResolver,这里比较巧妙,这里的方法引用this::getPropertyAsRawString相当于下面的代码:

//PlaceholderResolver是一个函数式接口
@FunctionalInterface
public interface PlaceholderResolver {
  @Nullable
  String resolvePlaceholder(String placeholderName);  
}
//this::getPropertyAsRawString相当于下面的代码
return new PlaceholderResolver(){

    @Override
    String resolvePlaceholder(String placeholderName){
        //这里调用到的是PropertySourcesPropertyResolver#getPropertyAsRawString,有点绕
        return getPropertyAsRawString(placeholderName);
    }
}       

接着看PropertyPlaceholderHelper#replacePlaceholders的源码:

//基础属性
//占位符前缀,默认是"${"
private final String placeholderPrefix;
//占位符后缀,默认是"}"
private final String placeholderSuffix;
//简单的占位符前缀,默认是"{",主要用于处理嵌套的占位符如${xxxxx.{yyyyy}}
private final String simplePrefix;

//默认值分隔符号,默认是":"
@Nullable
private final String valueSeparator;
//替换属性占位符
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
    Assert.notNull(value, "'value' must not be null");
    return parseStringValue(value, placeholderResolver, new HashSet<>());
}

//递归解析带占位符的属性为字符串
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) {
            //提取第一个占位符中的原始字符串,如${server.port}->server.port
            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");
            }
            // Recursive invocation, parsing placeholders contained in the placeholder key.
            // 递归调用,实际上就是解析嵌套的占位符,因为提取的原始字符串有可能还有一层或者多层占位符
            placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
            // Now obtain the value for the fully resolved key...
            // 递归调用完毕后,可以确定得到的字符串一定是不带占位符,这个时候调用getPropertyAsRawString获取key对应的字符串值
            String propVal = placeholderResolver.resolvePlaceholder(placeholder);
            // 如果字符串值为null,则进行默认值的解析,因为默认值有可能也使用了占位符,如${server.port:${server.port-2:8080}}
            if (propVal == null && this.valueSeparator != null) {
                int separatorIndex = placeholder.indexOf(this.valueSeparator);
                if (separatorIndex != -1) {
                    String actualPlaceholder = placeholder.substring(0, separatorIndex);
                    // 提取默认值的字符串
                    String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
                    // 这里是把默认值的表达式做一次解析,解析到null,则直接赋值为defaultValue
                    propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
                    if (propVal == null) {
                        propVal = defaultValue;
                    }
                }
            }
            // 上一步解析出来的值不为null,但是它有可能是一个带占位符的值,所以后面对值进行递归解析
            if (propVal != null) {
                // Recursive invocation, parsing placeholders contained in the
                // previously resolved placeholder value.
                propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
                // 这一步很重要,替换掉第一个被解析完毕的占位符属性,例如${server.port}-${spring.application.name} -> 9090--${spring.application.name}
                result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
                if (logger.isTraceEnabled()) {
                    logger.trace("Resolved placeholder '" + placeholder + "'");
                }
                // 重置startIndex为下一个需要解析的占位符前缀的索引,可能为-1,说明解析结束
                startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
            }
            else if (this.ignoreUnresolvablePlaceholders) {
                // 如果propVal为null并且ignoreUnresolvablePlaceholders设置为true,直接返回当前的占位符之间的原始字符串尾的索引,也就是跳过解析
                // Proceed with unprocessed value.
                startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
            }
            else {
                // 如果propVal为null并且ignoreUnresolvablePlaceholders设置为false,抛出异常
                throw new IllegalArgumentException("Could not resolve placeholder '" +
                            placeholder + "'" + " in value \"" + value + "\"");
            }
            // 递归结束移除判重集合中的元素
            visitedPlaceholders.remove(originalPlaceholder);
        }
        else {
            // endIndex = -1说明解析结束
            startIndex = -1;
        }
    }
    return result.toString();
}

//基于传入的起始索引,搜索第一个占位符后缀的索引,兼容嵌套的占位符
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
    //这里index实际上就是实际需要解析的属性的第一个字符,如${server.port},这里index指向s
    int index = startIndex + this.placeholderPrefix.length();
    int withinNestedPlaceholder = 0;
    while (index < buf.length()) {
        //index指向"}",说明有可能到达占位符尾部或者嵌套占位符尾部
        if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
            //存在嵌套占位符,则返回字符串中占位符后缀的索引值
            if (withinNestedPlaceholder > 0) {
                withinNestedPlaceholder--;
                index = index + this.placeholderSuffix.length();
            }
            else {
                //不存在嵌套占位符,直接返回占位符尾部索引
                return index;
            }
        }
        //index指向"{",记录嵌套占位符个数withinNestedPlaceholder加1,index更新为嵌套属性的第一个字符的索引
        else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
            withinNestedPlaceholder++;
            index = index + this.simplePrefix.length();
        }
        else {
            //index不是"{"或者"}",则进行自增
            index++;
        }
    }
    //这里说明解析索引已经超出了原字符串
    return -1;
}

//StringUtils#substringMatch,此方法会检查原始字符串str的index位置开始是否和子字符串substring完全匹配
public static boolean substringMatch(CharSequence str, int index, CharSequence substring) {
    if (index + substring.length() > str.length()) {
        return false;
    }
    for (int i = 0; i < substring.length(); i++) {
        if (str.charAt(index + i) != substring.charAt(i)) {
            return false;
        }
    }
    return true;
}

上面的过程相对比较复杂,因为用到了递归,我们举个实际的例子说明一下整个解析过程,例如我们使用了四个属性项,我们的目标是获取server.desc的值:

application.name=spring
server.port=9090
spring.application.name=${application.name}
server.desc=${server.port-${spring.application.name}}:${description:"hello"}

属性类型转换

在上一步解析属性占位符完毕之后,得到的是属性字符串值,可以把字符串转换为指定的类型,此功能由AbstractPropertyResolver#convertValueIfNecessary完成:

protected <T> T convertValueIfNecessary(Object value, @Nullable Class<T> targetType) {
    if (targetType == null) {
        return (T) value;
    }
    ConversionService conversionServiceToUse = this.conversionService;
    if (conversionServiceToUse == null) {
        // Avoid initialization of shared DefaultConversionService if
        // no standard type conversion is needed in the first place...
        // 这里一般只有字符串类型才会命中
        if (ClassUtils.isAssignableValue(targetType, value)) {
            return (T) value;
        }
        conversionServiceToUse = DefaultConversionService.getSharedInstance();
    }
    return conversionServiceToUse.convert(value, targetType);
}

实际上转换的逻辑是委托到DefaultConversionService的父类方法GenericConversionService#convert

public <T> T convert(@Nullable Object source, Class<T> targetType) {
    Assert.notNull(targetType, "Target type to convert to cannot be null");
    return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
}

public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
    Assert.notNull(targetType, "Target type to convert to cannot be null");
    if (sourceType == null) {
        Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
        return handleResult(null, targetType, convertNullSource(null, targetType));
    }
    if (source != null && !sourceType.getObjectType().isInstance(source)) {
        throw new IllegalArgumentException("Source to convert from must be an instance of [" +
                    sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
    }
    // 从缓存中获取GenericConverter实例,其实这一步相对复杂,匹配两个类型的时候,会解析整个类的层次进行对比
    GenericConverter converter = getConverter(sourceType, targetType);
    if (converter != null) {
        // 实际上就是调用转换方法
        Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
        // 断言最终结果和指定类型是否匹配并且返回
        return handleResult(sourceType, targetType, result);
    }
    return handleConverterNotFound(source, sourceType, targetType);
}

上面所有的可用的GenericConverter的实例可以在DefaultConversionService的addDefaultConverters中看到,默认添加的转换器实例已经超过20个,有些情况下如果无法满足需求可以添加自定义的转换器,实现GenericConverter接口添加进去即可。

总结

SpringBoot在抽象整个类型转换器方面做的比较好,在SpringMVC应用中,采用的是org.springframework.boot.autoconfigure.web.format.WebConversionService,兼容了Converter、Formatter、ConversionService等转换器类型并且对外提供一套统一的转换方法。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 详解spring cloud Feign使用中遇到的问题总结

    详解spring cloud Feign使用中遇到的问题总结

    本篇文章主要介绍了详解spring cloud Feign使用中遇到的问题总结,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-01-01
  • spring配置文件加密方法示例

    spring配置文件加密方法示例

    这篇文章主要介绍了spring配置文件加密方法示例,简单介绍了什么是配置文件,然后分享了在实际生产环境中,对配置文件不允许出现明文用户名及密码等信息需求的Java实现代码,具有一定参考价值,需要的朋友可以了解下。
    2017-11-11
  • Java双括弧初始化操作技巧

    Java双括弧初始化操作技巧

    这篇文章主要介绍了Java双括弧初始化操作技巧,这种方法不仅提高了代码的可读性,而且简化了代码的数量,需要的朋友可以参考下
    2015-12-12
  • SpringCloud学习笔记之OpenFeign进行服务调用

    SpringCloud学习笔记之OpenFeign进行服务调用

    OpenFeign对feign进行进一步的封装,添加了springmvc的一些功能,更加强大,下面这篇文章主要给大家介绍了关于SpringCloud学习笔记之OpenFeign进行服务调用的相关资料,需要的朋友可以参考下
    2022-01-01
  • SpringBoot使用AOP实现统计全局接口访问次数详解

    SpringBoot使用AOP实现统计全局接口访问次数详解

    这篇文章主要介绍了SpringBoot通过AOP实现对全局接口访问次数的统计,文章从相关问题展开全文内容详情,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-06-06
  • 深入解析java HashMap实现原理

    深入解析java HashMap实现原理

    这篇文章主要介绍了深入解析java HashMap实现原理的相关资料,需要的朋友可以参考下
    2015-09-09
  • 在Java的MyBatis框架中建立接口进行CRUD操作的方法

    在Java的MyBatis框架中建立接口进行CRUD操作的方法

    这篇文章主要介绍了在Java的MyBatis框架中建立接口进行CRUD操作的方法,CRUD是指在做计算处理时的增加(Create)、重新取得数据(Retrieve)、更新(Update)和删除(Delete)几个单词的首字母简写,需要的朋友可以参考下
    2016-04-04
  • 总结Bean的三种自定义初始化和销毁方法

    总结Bean的三种自定义初始化和销毁方法

    这篇文章主要介绍了Bean的三种自定义初始化和销毁方法,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • Java实现根据模板自动生成新的PPT

    Java实现根据模板自动生成新的PPT

    这篇文章主要介绍了如何利用Java代码自动生成PPT,具体就是查询数据库数据,然后根据模板文件(PPT),将数据库数据与模板文件(PPT),进行组合一下,生成新的PPT文件。感兴趣的可以了解一下
    2022-02-02
  • 如何判断java是32位的还是64位的

    如何判断java是32位的还是64位的

    这篇文章主要介绍了如何判断java是32位的还是64位的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-04-04

最新评论