【SpringBoot- Spring】PropertyResolver 占位符解析

PropertyResolver占位符解析PI 介绍

一、PropertyResolver API

PropertyResolver 的默认实现是 PropertySourcesPropertyResolver,Environment 实际上也是委托 PropertySourcesPropertyResolver 完成 占位符的解析和类型转换。 类型转换又是委托 ConversionService 完成的。

public interface PropertyResolver {
    // 1. contains
    boolean containsProperty(String key);

    // 2.1 获取指定 key,不存在可以指定默认值,也可以抛出异常
    String getProperty(String key);
    String getProperty(String key, String defaultValue);

    // 2.2 类型转换,委托 ConversionService 完成
    <T> T getProperty(String key, Class<T> targetType);
    <T> T getProperty(String key, Class<T> targetType, T defaultValue);

    String getRequiredProperty(String key) throws IllegalStateException;
    <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;

    // 3. 解析占位符 ${key}
    String resolvePlaceholders(String text);
    String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}

PropertyResolver 组件完成了两件事:一是占位符解析 ${key};二是类型转换。使用方法如下:

@Test
public void PropertyResolverTest() {
    PropertySource propertySource = new MapPropertySource("source",
            Collections.singletonMap("name", "binarylei"));
    MutablePropertySources propertySources = new MutablePropertySources();
    propertySources.addFirst(propertySource);
    PropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources);

    Assert.assertEquals("binarylei", propertyResolver.getProperty("name"));
    Assert.assertEquals("name is binarylei", propertyResolver.resolvePlaceholders("name is ${name}"));
}

二、Spring 是如何使用的

(1) xml 配置

<context:property-placeholder   
    location="属性文件,多个之间逗号分隔"  
    file-encoding="文件编码"  
    ignore-resource-not-found="是否忽略找不到的属性文件"  
    ignore-unresolvable="是否忽略解析不到的属性,如果不忽略,找不到将抛出异常"  
    properties-ref="本地Properties配置"  
    local-override="是否本地覆盖模式,即如果true,那么properties-ref的属性将覆盖location加载的属性,否则相反"  
    system-properties-mode="系统属性模式,默认ENVIRONMENT(表示先找ENVIRONMENT,再找properties-ref/location的),NEVER:表示永远不用ENVIRONMENT的,OVERRIDE类似于ENVIRONMENT"  
    order="顺序"  
/>  
  • location:表示属性文件位置,多个之间通过如逗号/分号等分隔;
  • file-encoding:文件编码;
  • ignore-resource-not-found:如果属性文件找不到,是否忽略,默认 false,即不忽略,找不到将抛出异常
  • ignore-unresolvable:是否忽略解析不到的属性,如果不忽略,找不到将抛出异常
  • properties-ref:本地 java.util.Properties 配置
  • local-override:是否本地覆盖模式,即如果 true,那么 properties-ref 的属性将覆盖 location 加载的属性
  • system-properties-mode:系统属性模式,ENVIRONMENT(默认),NEVER,OVERRIDE
    1. ENVIRONMENT:将使用 Spring 3.1 提供的 PropertySourcesPlaceholderConfigurer,其他情况使用 Spring 3.1 之前的 PropertyPlaceholderConfigurer。如果是本地覆盖模式:那么查找顺序是:properties-ref、location、environment,否则正好反过来;
    2. OVERRIDE: PropertyPlaceholderConfigurer 使用,因为在 spring 3.1 之前版本是没有 Enviroment 的,所以 OVERRIDE 是 spring 3.1 之前版本的 Environment。如果是本地覆盖模式:那么查找顺序是:properties-ref、location、System.getProperty(), System.getenv(),否则正好反过来;
    3. NEVER:只查找 properties-ref、location;
  • order:当配置多个context:property-placeholder/时的查找顺序,关于顺序问题请参考:<http://www.iteye.com/topic/1131688 >

(2) 注解配置

@Configuration  
@PropertySource(value = "classpath:resources.properties", ignoreResourceNotFound = false)  
public class AppConfig {    
    // 如果想进行 Bean 属性的占位符替换,需要注册 PropertySourcesPlaceholderConfigurer
    @Bean  
    public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {  
        return new PropertySourcesPlaceholderConfigurer();  
    }   
} 

如上配置等价于 XML 中的 context:property-placeholder/ 配置。

(3) 占位符替换

使用 Environment 属性替换,如:

<context:property-placeholder location="classpath:${env}/resources.properties"/>  
<context:component-scan base-package="com.sishuok.${package}"/> 
<import resource="classpath:${env}/ctx.xml"/>

同样可以在注解中使用占位符。

@PropertySource(value = "classpath:${env}/resources.properties")
@ComponentScan(basePackages = "com.sishuok.${package}")
@ImportResource(value = {"classpath:${env}/cfg.xml"})
@Value("${env}")   
new ClassPathXmlApplicationContext("classpath:${env}/cfg.xml")

参考:

  1. 《Spring 3.1 新属性管理 API:PropertySource、Environment、Profile》:https://jinnianshilongnian.iteye.com/blog/2000183

PropertyResolver占位符解析源码分析

img

一、PropertyResolver 接口

PropertyResolver 的默认实现是 PropertySourcesPropertyResolver,Environment 实际上也是委托 PropertySourcesPropertyResolver 完成 占位符的解析和类型转换。 类型转换又是委托 ConversionService 完成的。

public interface PropertyResolver {
    // 1. contains
    boolean containsProperty(String key);

    // 2.1 获取指定 key,不存在可以指定默认值,也可以抛出异常
    String getProperty(String key);
    String getProperty(String key, String defaultValue);

    // 2.2 类型转换,委托 ConversionService 完成
    <T> T getProperty(String key, Class<T> targetType);
    <T> T getProperty(String key, Class<T> targetType, T defaultValue);

    String getRequiredProperty(String key) throws IllegalStateException;
    <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;

    // 3. 解析占位符 ${key}
    String resolvePlaceholders(String text);
    String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}

ConfigurablePropertyResolver 是配置了一些解析占位符的必要属性,如占位符前缀和后缀等

public interface ConfigurablePropertyResolver extends PropertyResolver {
    // 1. 类型转换
    ConfigurableConversionService getConversionService();
    void setConversionService(ConfigurableConversionService conversionService);

    // 2.1 ${} 分隔符
    void setPlaceholderPrefix(String placeholderPrefix);
    void setPlaceholderSuffix(String placeholderSuffix);
    // 2.2 默认属性分隔符 :
    void setValueSeparator(@Nullable String valueSeparator);
    
    // 3.1 ${key} getProperty(key)==null 时是否忽略,不抛出异常
    void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders);
    void setRequiredProperties(String... requiredProperties);
    void validateRequiredProperties() throws MissingRequiredPropertiesException;
}

二、PropertySourcesPropertyResolver 源码分析

PropertySourcesPropertyResolver 持有一个数据源 PropertySources,可以通过 getProperty 获取对应的属性值,这方法有几种重载的方法,决定是否解析嵌套占位符和类型转换。

// 不进行类型转换,但会进行嵌套占位符的解析
@Override
public String getProperty(String key) {
    return getProperty(key, String.class, true);
}

// 进行类型转换,也进行嵌套占位符的解析
@Override
public <T> T getProperty(String key, Class<T> targetValueType) {
    return getProperty(key, targetValueType, true);
}

// 不进行类型转换,也不进行嵌套占位符的解析,返回原始的字符串
@Override
protected String getPropertyAsRawString(String key) {
    return getProperty(key, String.class, false);
}

我们再看一下 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) {
            // 1. 从数据源中获取属性值
            Object value = propertySource.getProperty(key);
            if (value != null) {
                // 2. 如果属性值本身又含有占位符就属于嵌套占位符解析,如 ${a${x}b}
                if (resolveNestedPlaceholders && value instanceof String) {
                    value = resolveNestedPlaceholders((String) value);
                }
                // 日志输出
                logKeyFound(key, propertySource, value);
                // 3. 类型转换
                return convertValueIfNecessary(value, targetValueType);
            }
        }
    }
    return null;
}

现在最关键的方法是 resolveNestedPlaceholders,用于解析嵌套的占位符,这个方法是在其父类 AbstractPropertyResolver 实现的。

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

private PropertyPlaceholderHelper nonStrictHelper;
private PropertyPlaceholderHelper strictHelper;
@Override
public String resolvePlaceholders(String text) {
    if (this.nonStrictHelper == null) {
        this.nonStrictHelper = createPlaceholderHelper(true);
    }
    return doResolvePlaceholders(text, this.nonStrictHelper);
}
@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
    if (this.strictHelper == null) {
        this.strictHelper = createPlaceholderHelper(false);
    }
    return doResolvePlaceholders(text, this.strictHelper);
}

实际上嵌套占位符的解析 PropertySourcesPropertyResolver 都委托给了 PropertyPlaceholderHelper 方法来完成,而自身主要完成从 PropertySources 获取属性值。

private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
    return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix,
            this.valueSeparator, ignoreUnresolvablePlaceholders);
}
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
    return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}

三、PropertyPlaceholderHelper 嵌套占位符的解析

在看 PropertyPlaceholderHelper 之前先看一下 PlaceholderResolver 这个内部类,这个类用于获取占位符 key 对应的 value。在 AbstractPropertyResolver#doResolvePlaceholders 方法中将 this::getPropertyAsRawString 传过来了,也就是说 PlaceholderResolver 是从 propertySources 获取对应的 value 值。

@FunctionalInterface
public interface PlaceholderResolver {
    // 从 propertySource 中获取 placeholderName 的 value
    String resolvePlaceholder(String placeholderName);
}

下面再看 replacePlaceholders 是如何解析嵌套的占位符的。

public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
    Assert.notNull(value, "'value' must not be null");
    // placeholderResolver 是从 propertySources 获取的属性值
    return parseStringValue(value, placeholderResolver, new HashSet<>());
}

// 循环解析 key ${a${x}b}
protected String parseStringValue(
        String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {

    StringBuilder result = new StringBuilder(value);

    int startIndex = value.indexOf(this.placeholderPrefix);
    while (startIndex != -1) {
        // 找到结束的 } 位置,注意嵌套时要找对应的结束标记符 ${a${x}b}
        int endIndex = findPlaceholderEndIndex(result, startIndex);
        // endIndex=-1 或 startIndex=-1 结束循环
        if (endIndex != -1) {
            // 1. 获取 ${key} 的 key
            String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
            String originalPlaceholder = placeholder;
            // 2. key 出现了循环嵌套,直接 Game Over
            if (!visitedPlaceholders.add(originalPlaceholder)) {
                throw new IllegalArgumentException(
                        "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
            }
            // 2. 循环解析这个 key,如果这个 key 又是形如 ${...}
            placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
            // 3. 至此,这个 key 不可能出现 ${} 了,因此可以放心大胆的从 propertySources 获取对应的 value
            String propVal = placeholderResolver.resolvePlaceholder(placeholder);
            // 4. ${key:default} 如果为 null,获取真正的 key,如果为 null 则为默认值
            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());
                    propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
                    if (propVal == null) {
                        propVal = defaultValue;
                    }
                }
            }
            // 5. 对不起,value 也可能为 ${...},递归解析
            if (propVal != null) {
                propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
                result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
                startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
            } else if (this.ignoreUnresolvablePlaceholders) {
                // 6. 忽略无法解析的 key,继续...
                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;
        }
    }

    return result.toString();
}

posted @ 2025-09-24 09:58  明小子@  阅读(6)  评论(0)    收藏  举报