在使用spring时,为了方便对某些参数值进行修改,会将参数以键值对的形式写在配置文件中,在Java代码中通过key来获取配置文件中的value。
那么,spring是如何知道哪些参数的值需要从配置文件获取并进行替换的呢?答案就是使用占位符。
例如:使用${key}作为占位符时,当spring在解析属性时碰到${key},就会用占位符中的key,获取对应的value,并将${key}替换为value。
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);
}
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;
}
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);
}
使用解析器解析到占位符,使用属性占位符助手替换占位符。
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()获取
问题:如何设置其他的属性源