Spring源码:PropertySourcesPropertyResolver属性源属性解析器

Spring解析占位符

  • Spring解析配置文件路径中的占位符
  • PropertySourcesPropertyResolver解析器解析占位符
    • PropertySourcesPropertyResolver解析占位符
    • 创建PropertySourcesPropertyResolver解析器
  • PropertyPlaceholderHelper属性占位符助手替换占位符
  • 问题

在使用spring时,为了方便对某些参数值进行修改,会将参数以键值对的形式写在配置文件中,在Java代码中通过key来获取配置文件中的value。

那么,spring是如何知道哪些参数的值需要从配置文件获取并进行替换的呢?答案就是使用占位符。
例如:使用${key}作为占位符时,当spring在解析属性时碰到${key},就会用占位符中的key,获取对应的value,并将${key}替换为value。

Spring解析配置文件路径中的占位符

spring创建容器时,会通过参数设置spring需要加载哪些配置文件,这个参数是配置文件的路径,路径中可能会有占位符,所以spring需要解析参数中的占位符。
1、使用ClassPathXmlApplicationContext创建spring容器,并设置配置文件路径。代码如下:

public static void main(String[] args) {
	ClassPathXmlApplicationContext classPathXmlApplicationContext =new ClassPathXmlApplicationContext("${classpath*:/*.xml}");
}

2、单个参数的构造方法,会调用三个参数的构造方法,spring源码如下:

public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
	this(new String[] {configLocation}, true, null);
}

3、三个参数的构造方法,该方法会调用setConfigLocations方法,将配置文件路径设置给成员变量configLocations,spring源码如下:

// 核心构造函数,设置此应用上下文的配置文件的位置,并判断是否自动刷新上下文
public ClassPathXmlApplicationContext(
		String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
		throws BeansException {

	// 将用父类的构造方法,设置父容器
	super(parent);
	//设置应用上下文的配置文件的位置,将配置文件的路径存放到configLocations字符串数组中
	setConfigLocations(configLocations);
	// 如果刷新表示为true,则会调用refresh()方法加载spring容器
	if (refresh) {
		refresh();
	}
}

4、setConfigLocations方法,该方法会调用resolvePath方法,解析参数中的占位符

// 将配置文件的路径放到configLocations 字符串数组中
public void setConfigLocations(@Nullable String... locations) {
	if (locations != null) {
		Assert.noNullElements(locations, "Config locations must not be null");
		// 设置了几个配置文件,就创一个多长的字符串数组,用来存放配置文件的路径
		this.configLocations = new String[locations.length];
		for (int i = 0; i < locations.length; i++) {
			//解析路径,将解析的路径存放到字符串数组中
			this.configLocations[i] = resolvePath(locations[i]).trim();
		}
	}
	else {
		this.configLocations = null;
	}
}

5、resolvePath方法,通过getEnvironment方法获取当前环境,根据当前环境解析占位符。通过getEnvironment方法获取到的是spring的标准环境StandardEnvironment。
StandardEnvironment没有重写父类AbstractEnvironment中的resolveRequiredPlaceholders的方法,因此代码中调用的是AbstractEnvironment类中的resolveRequiredPlaceholders方法。

// 解析给定的路径,必要时用相应的环境属性值替换占位符。应用于配置位置。
protected String resolvePath(String path) {
	// 获取环境,解决所需的占位符
	return getEnvironment().resolveRequiredPlaceholders(path);
}

关于Spring的环境在另外一篇博客中有讲解,点击链接可直接观看: Spring环境信息

6、获取到的标准环境StandardEnvironment会调用父类AbstractEnvironment类中的resolveRequiredPlaceholders方法,该方法会调用具体属性解析器的resolveRequiredPlaceholders方法解析占位符。
AbstractEnvironment类中的解析器是在spring创建环境是创建的PropertySourcesPropertyResolver解析器。

// 解析所需占位符
@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
	return this.propertyResolver.resolveRequiredPlaceholders(text);
}

PropertySourcesPropertyResolver解析器解析占位符

PropertySourcesPropertyResolver解析占位符

1、上文中讲到在获取到环境后会调用标准环境对象的resolveRequiredPlaceholders方法,该方法中会调用PropertySourcesPropertyResolver解析器中的resolveRequiredPlaceholders方法,因为该解析器没有重写父类AbstractPropertyResolver中的resolveRequiredPlaceholders方法,所以将会调用AbstractPropertyResolver类中的resolveRequiredPlaceholders方法。spring源码如下:
1)resolveRequiredPlaceholders方法中会调用createPlaceholderHelper方法创建占位符助手
2)resolveRequiredPlaceholders方法会调用doResolvePlaceholders解析占位符

// 解析占位符
@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
	if (this.strictHelper == null) {
		//创建占位符助手
		this.strictHelper = createPlaceholderHelper(false);
	}
	//解析占位符
	return doResolvePlaceholders(text, this.strictHelper);
}

2、createPlaceholderHelper方法,该方法会调用PropertyPlaceholderHelper类的构造方法创建一个占位符助手,spring源码如下:

// 创建占位符助手
private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
	return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix,
			this.valueSeparator, ignoreUnresolvablePlaceholders);
}

3、调用PropertyPlaceholderHelper的构造方法创建属性占位符助手
注意:属性占位符助手中,包括占位符的前缀、后缀、值分隔符、是否忽略不可解析占位符的表示。

// 创建一个属性占位符助手,需要指定占位符前缀、后缀
public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix,
		@Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) {

	// 使用断言校验占位符的前缀和后缀不能为null
	Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null");
	Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null");
	// 将占位符前缀和后缀设置给 属性占位符助手
	this.placeholderPrefix = placeholderPrefix;
	this.placeholderSuffix = placeholderSuffix;
	// 根据占位符的后缀,获取对应的常见的简单的前缀
	String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix);
	// 如果 简单的前缀 不是null,并且 占位符前缀是以简单的前缀为结尾
	if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) {
		// 将简单的前缀赋值给成员变量
		this.simplePrefix = simplePrefixForSuffix;
	}
	else {
		// 将整个占位符前缀赋值给简单前缀
		this.simplePrefix = this.placeholderPrefix;
	}
	// 将值的分割分赋值给成员变量
	this.valueSeparator = valueSeparator;
	// 将是否忽略不可解析占位符的表示赋值给成员变量
	this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
}

4、调用doResolvePlaceholders方法解析占位符,实际干活的是PropertyPlaceholderHelper 属性占位符助手。该方法会调用属性占位符助手的replacePlaceholders方法,该方法在后文中有讲解。doResolvePlaceholders方法源代码如下:

// 解析占位符
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
	// 使用 PropertyPlaceholderHelper(属性占位符助手) 替换占位符。
	// 函数式接口,在调用PlaceholderResolver类中的resolvePlaceholder方法是,调用getPropertyAsRawString方法
	return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}

5、getPropertyAsRawString方法,根据key获取value,并将value转换为String类型。该方法调用的是下面的getProperty方法。
注意:该方法没有直接调用,而是通过函数式接口进行调用。

// 获取属性作为原始字符串,即根据key获取value。没有直接调用,通过函数时接口进行调用
@Override
@Nullable
protected String getPropertyAsRawString(String key) {
	// 根据key获取属性值value
	return getProperty(key, String.class, false);
}

6、getProperty方法,根据key从属性源中获取对应的value,如果没有获取到,返回null

// 根据key,从属性源中获取对应的value
@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
	// 如果属性源集合不为null
	if (this.propertySources != null) {
		for (PropertySource<?> propertySource : this.propertySources) {
			if (logger.isTraceEnabled()) {
				logger.trace("Searching for key '" + key + "' in PropertySource '" +
						propertySource.getName() + "'");
			}
			// 从属性源中根据key获取属性值
			Object value = propertySource.getProperty(key);
			// 获取到的值不为null
			if (value != null) {
				// 需要解析嵌套占位符解析嵌套占位符,并且value值是String类型
				// resolveNestedPlaceholders 是否解析嵌套占位符标识,默认为false
				if (resolveNestedPlaceholders && value instanceof String) {
					// 解析嵌套占位符
					value = resolveNestedPlaceholders((String) value);
				}
				// 打印日志
				logKeyFound(key, propertySource, value);
				// 进行类型转换
				return convertValueIfNecessary(value, targetValueType);
			}
		}
	}
	if (logger.isTraceEnabled()) {
		logger.trace("Could not find key '" + key + "' in any property source");
	}
	return null;
}

创建PropertySourcesPropertyResolver解析器

1、spring在创建环境是会创建PropertySourcesPropertyResolver解析器
(Spring创建环境)
spring创建环境是执行以下代码:

// 使用了模板方法设计模式。
// 给成员变量赋值,并调用子类重写的方法,对propertySources进行操作。
protected AbstractEnvironment(MutablePropertySources propertySources) {
	// 给全局变量 可变属性源 赋值
	this.propertySources = propertySources;
	// 创建属性解析器:PropertySourcesPropertyResolver 属性源属性解析器
	this.propertyResolver = createPropertyResolver(propertySources);
	// 自定义属性源,此处回调子类重写的方法。子类通过重写该方法可以操作propertySources。spring标准环境StandardEnvironment重写了该方法
	customizePropertySources(propertySources);
}

2、调用createPropertyResolver方法创建解析器,并将属性源设置给解析器,解析器在解析占位符时会从属性源中通过key获取value。

// 在创建环境时,需要创建属性解析器
protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) {
	return new PropertySourcesPropertyResolver(propertySources);
}

PropertyPlaceholderHelper属性占位符助手替换占位符

使用解析器解析到占位符,使用属性占位符助手替换占位符。
1、调用replacePlaceholders方法,进行占位符的替换

// 将所有占位符替换为 PlaceholderResolver(占位符解析器)返回的值。
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
	Assert.notNull(value, "'value' must not be null");
	return parseStringValue(value, placeholderResolver, null);
}

2、调用parseStringValue将字符转中的占位符替换为对应的value。
注意:
1)占位符前缀和后缀成对出现
2)占位符中不要使用冒号
3)尽量不要在属性值中使用占位符。例如:a=${a},会造成占位符循环,会抛出异常
4)占位符可以多层嵌套,例如:${a${b}}
5)占位符可以多个拼接,例如:${a}${b}
6)使用了函数式接口PlaceholderResolver,调用的是PropertySourcesPropertyResolver类中的getPropertyAsRawString方法,在上文中有讲解该方法。

// 解析字符串值,替换占位符
protected String parseStringValue(
		String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) {

	// 获取字符串中占位符前缀的索引值
	int startIndex = value.indexOf(this.placeholderPrefix);
	// 如果字符串中没有占位符,直接将字符串返回
	if (startIndex == -1) {
		return value;
	}

	// 将字符串转成StringBuilder(线程不安全)
	StringBuilder result = new StringBuilder(value);
	// 使用while
	while (startIndex != -1) {
		// 获取当前占位符前缀对应的占位符后缀索引
		int endIndex = findPlaceholderEndIndex(result, startIndex);
		// 有占位符后缀
		if (endIndex != -1) {
			// 获取占位符前缀和后缀之间的字符串
			String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
			// 将获取到的占位符 赋值给原始占位符
			String originalPlaceholder = placeholder;
			// 默认visitedPlaceholders是null
			if (visitedPlaceholders == null) {
				// 创建一个容量为4的HashSet
				visitedPlaceholders = new HashSet<>(4);
			}
			// 将原始占位符放到 visitedPlaceholders集合中(已经处理过的占位符)。
			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...
			// 现在获取完全解析的键的值…(获取占位符中key对应的value,此时该占位符字符串中不再包含占位符)
			// placeholderResolver是一个函数式接口,调用resolvePlaceholder方法是,其实调用的是PropertySourcesPropertyResolver类中getPropertyAsRawString
			String propVal = placeholderResolver.resolvePlaceholder(placeholder);
			// 根据占位符获取到的值为null,并且之分隔符不为null。valueSeparator默认为冒号
			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());
					// placeholderResolver是一个函数式接口,调用resolvePlaceholder方法是,其实调用的是PropertySourcesPropertyResolver类中getPropertyAsRawString
					propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
					// 以占位符中分隔符前的字符串为key,获取value,如果没有获取到,则使用占位符中分隔符后的字符串占位符对应的value
					if (propVal == null) {
						propVal = defaultValue;
					}
				}
			}
			// 根据占位符获取到了value
			if (propVal != null) {
				// Recursive invocation, parsing placeholders contained in the
				// previously resolved placeholder value.
				// 递归调用,解析先前解析的占位符值中包含的占位符。解析占位符对应的value中的占位符
				propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
				// 占位符的替换,使用获取到的value,将占位符前缀、占位符、占位符后缀 全部替换掉
				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) {
				// Proceed with unprocessed value.
				startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
			}
			// 根据占位符没有获取到value,并且不忽略不可解析占位符
			else {
				throw new IllegalArgumentException("Could not resolve placeholder '" +
						placeholder + "'" + " in value \"" + value + "\"");
			}
			// 将原始占位符从visitedPlaceholders集合中移除
			visitedPlaceholders.remove(originalPlaceholder);
		}
		// 占位符后缀索引为-1,表示没有占位符后缀
		else {
			startIndex = -1;
		}
	}
	return result.toString();
}

问题

解析配置文件路径中的占位符会从属性源中获取对应的value值。属性源有两个:
1)通过System.getProperties()获取
2)通过System.getenv()获取
问题:如何设置其他的属性源

你可能感兴趣的:(Spring,spring,java,后端)