Spring版本
5.2.5.RELEASE
参考
在平时的项目中,我们经常会使用配置文件来根据不同的环境动态加载配置项。那么,spring是如何使用配置项替换掉bean中的占位符的呢?这一切的神奇之处都在PropertySourcesPlaceholderConfigurer
(5.2之前是使用PropertyPlaceholderConfigurer
,5.2废弃该类,官方建议使用PropertySourcesPlaceholderConfigurer
),它负责加载并替换bean中的占位符。首先从一个小demo来认识这个类。
1. DEMO
1.1 Student
public class Student {
private String id;
private String name;
private String desc;
// 省略 getter、setter
}
1.2 CustomPropertyConfig
public class CustomPropertyConfig extends PropertySourcesPlaceholderConfigurer {
private Resource [] locations;
private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
@Override
public void setLocations(Resource... locations) {
this.locations = locations;
}
@Override
public void setLocalOverride(boolean localOverride) {
this.localOverride = localOverride;
}
@Override
protected void loadProperties(Properties props) throws IOException {
if (this.locations != null) {
for (Resource location : this.locations) {
InputStream inputStream = null;
try {
String fileName = location.getFilename();
String env = "application-" + System.getProperty("spring.profiles.active", "dev") + ".properties";
if (fileName.contains(env)) {
logger.info("loading properties file from " + location);
inputStream = location.getInputStream();
this.propertiesPersister.load(props,inputStream);
}
} catch (Exception e) {
logger.info("error",e);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
}
}
}
1.3 环境配置文件
application-dev.properties
student.name=student-dev
application-test.properties
student.name=student-test
student.value=student-test-value
1.4 spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="propertyConfig" class="com.kungyu.custom.element.CustomPropertyConfig">
<property name="locations">
<list>
<value>classpath:application-dev.properties</value>
<value>classpath:application-test.properties</value>
</list>
</property>
</bean>
<bean id="student" class="com.kungyu.custom.element.Student">
<property name="name" value="${student.name}"/>
</bean>
</bean>
1.5 测试
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
Student student = (Student) context.getBean("student");
System.out.println(student.getName());
}
启动参数配置如下:
启动参数配置
用于指定加载的配置文件,输出如下:
输出结果
可以看到,student
这个bean中name
属性原本使用的是${student.name}
这个占位符,如今已经被替换成配置文件application-dev.properties
中的student-name
的属性值student-dev
当加载测试环境配置的时候,修改VM options
即可
2. 源码解读
通过debug,我们可以发现,入口在于PropertySourcesPlaceholderConfigurer#postProcessBeanFactory
找到该入口方法的debug方法可以查看《Spring源码解析(十六)-BeanFactoryPostProcessor》
2.1 PropertySourcesPlaceholderConfigurer#postProcessBeanFactory
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// propertySources:属性资源,内部包含一个propertySourceList,用于存放各个环境下的资源
if (this.propertySources == null) {
this.propertySources = new MutablePropertySources();
// environment系统环境的一些配置
if (this.environment != null) {
this.propertySources.addLast(
new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
@Override
@Nullable
public String getProperty(String key) {
return this.source.getProperty(key);
}
}
);
}
try {
// 通过mergeProperties加载用户配置的资源文件
PropertySource<?> localPropertySource =
new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties());
// localOverride为true代表优先加载本地资源,也就是优先级高(所以使用addFirst),false代表最后加载本地资源,也就是本地资源优先级地(所以使用addLast)
// 这种优先级的高低在PropertySourcesPropertyResolver#getProperty中体现
if (this.localOverride) {
this.propertySources.addFirst(localPropertySource);
}
else {
this.propertySources.addLast(localPropertySource);
}
}
catch (IOException ex) {
throw new BeanInitializationException("Could not load properties", ex);
}
}
// 进行资源解析,同时替换${}占位符
processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources));
this.appliedPropertySources = this.propertySources;
}
首先判断类属性propertySources
是否为空,为空则构造一个对象,接着主要分俩步:
- 加载系统环境配置,加入到
propertySources
中 - 加载用户的配置,也就是DEMO中的配置文件,同样加入到
propertySources
中
这里需要留意多份环境配置文件之间存在优先级之分,通过
addFirst
加入的配置文件优先级最高:
/**
* Add the given property source object with highest precedence.
*/
public void addFirst(PropertySource<?> propertySource) {
removeIfPresent(propertySource);
this.propertySourceList.add(0, propertySource);
}
通过
addLast
加入的优先级最低:
/**
* Add the given property source object with lowest precedence.
*/
public void addLast(PropertySource<?> propertySource) {
removeIfPresent(propertySource);
this.propertySourceList.add(propertySource);
}
这个优先级的区分,将体现在2.12节的
getProperty
方法中
处理完配置文件之后,调用processProperties
方法正式开始占位符相关处理
2.2 PropertySourcesPlaceholderConfigurer#processProperties
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
final ConfigurablePropertyResolver propertyResolver) throws BeansException {
// 设置解析器的占位符前缀,占位符后缀,和默认值分割符,对应值如下
// placeholderPrefix ${
// placeholderSuffix }
// valueSeparator :
propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
propertyResolver.setValueSeparator(this.valueSeparator);
// 构造一个函数式接口的解析器
StringValueResolver valueResolver = strVal -> {
// ignoreUnresolvablePlaceholders 是否无视不可解析的占位符,如果设置为false,那么碰到不可解析的占位符的时候,会抛出异常
String resolved = (this.ignoreUnresolvablePlaceholders ?
propertyResolver.resolvePlaceholders(strVal) :
propertyResolver.resolveRequiredPlaceholders(strVal));
if (this.trimValues) {
resolved = resolved.trim();
}
return (resolved.equals(this.nullValue) ? null : resolved);
};
// 真正解析占位符
doProcessProperties(beanFactoryToProcess, valueResolver);
}
逻辑分为三步:
- 设置占位符前缀、后缀和默认值分隔符
propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
propertyResolver.setValueSeparator(this.valueSeparator);
- 构造占位符解析器的函数式接口
StringValueResolver valueResolver = strVal - > {
/* ignoreUnresolvablePlaceholders 是否无视不可解析的占位符,如果设置为false,那么碰到不可解析的占位符的时候,会抛出异常 */
String resolved = (this.ignoreUnresolvablePlaceholders ?
propertyResolver.resolvePlaceholders( strVal ) :
propertyResolver.resolveRequiredPlaceholders( strVal ) );
if ( this.trimValues )
{
resolved = resolved.trim();
}
return(resolved.equals( this.nullValue ) ? null : resolved);
};
该函数式接口在2.7节中将会被使用,通过resolvePlaceholders
和resolveRequiredPlaceholders
对占位符strVal
进行解析
- 真正开始解析占位符
doProcessProperties(beanFactoryToProcess, valueResolver);
2.3 PropertySourcesPlaceholderConfigurer#doProcessProperties
protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess, StringValueResolver valueResolver) {
// 传入valueResolver构造一个vistor,之后将使用valueResolver来解析beanDefinition中的占位符属性
BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
// 获取全部beanName
String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
for (String curName: beanNames) {
// Check that we're not parsing our own bean definition,
// to avoid failing on unresolvable placeholders in properties file locations.
// curName.equals(this.beanName):占位符解析本质上也是注册了一个bean,所以对于占位符这个bean,需要跳过
if (! (curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
// 获取BeanDefinition
BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
try {
// 对parentName、class、property等等进行解析,并且替换beanDefinition中原来的占位符
visitor.visitBeanDefinition(bd);
} catch(Exception ex) {
throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
}
}
}
// New in Spring 2.5: resolve placeholders in alias target names and aliases as well.
// 对别名也应用解析起进行处理
beanFactoryToProcess.resolveAliases(valueResolver);
// New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
// 往嵌入式注解解析器列表增加该解析器,在创建bean的过程中,会使用该解析器去解析@Value的属性
beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
}
- 将解析器
valueResolver
作为参数构造了一个BeanDefinitionVisitor
对象,那么可以想象,解析的动作应该是发生在BeanDefinitionVisitor
对象的成员方法中了 - 获取全部已解析的
BeanDefinition
,遍历,使用BeanDefinitionVisitor
对象的成员方法visitBeanDefinition
进行解析并且替换BeanDefinition
中的占位符 - 调用
resolveAliases
处理别名,不是我们关心的流程,具体不展开了 - 调用
addEmbeddedValueResolver
,往嵌入式注解解析器列表embeddedValueResolvers
增加该解析器,在《Spring源码解析(十)-填充bean属性》4.2节中解析了@Value
注解,那时候会使用到该解析器列表embeddedValueResolvers
@Override
public void addEmbeddedValueResolver(StringValueResolver valueResolver) {
Assert.notNull(valueResolver, "StringValueResolver must not be null");
this.embeddedValueResolvers.add(valueResolver);
}
2.4 BeanDefinitionVisitor#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());
}
}
可以看到,该方法对parentName
、beanClass
等数据域都进行了解析,这里我们只关心属性的解析方法visitPropertyValues
2.5 BeanDefinitionVisitor#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);
}
}
}
- 拿到属性列表
pvArray
- 遍历
pvArray
,调用resolveValue
进行占位符解析,如果解析后的新值和原值不同,通过add
方法进行替换或者合并(如果可以合并的前提下)
2.6 BeanDefinitionVisitor#resolveValue
@Nullable
protected Object resolveValue(@Nullable Object value) {
if (value instanceof BeanDefinition) {
visitBeanDefinition((BeanDefinition) value);
}
else if (value instanceof BeanDefinitionHolder) {
visitBeanDefinition(((BeanDefinitionHolder) value).getBeanDefinition());
}
else if (value instanceof RuntimeBeanReference) {
RuntimeBeanReference ref = (RuntimeBeanReference) value;
String newBeanName = resolveStringValue(ref.getBeanName());
if (newBeanName == null) {
return null;
}
if (!newBeanName.equals(ref.getBeanName())) {
return new RuntimeBeanReference(newBeanName);
}
}
else if (value instanceof RuntimeBeanNameReference) {
RuntimeBeanNameReference ref = (RuntimeBeanNameReference) value;
String newBeanName = resolveStringValue(ref.getBeanName());
if (newBeanName == null) {
return null;
}
if (!newBeanName.equals(ref.getBeanName())) {
return new RuntimeBeanNameReference(newBeanName);
}
}
else if (value instanceof Object[]) {
visitArray((Object[]) value);
}
else if (value instanceof List) {
visitList((List) value);
}
else if (value instanceof Set) {
visitSet((Set) value);
}
else if (value instanceof Map) {
visitMap((Map) value);
}
else if (value instanceof TypedStringValue) {
TypedStringValue typedStringValue = (TypedStringValue) value;
String stringValue = typedStringValue.getValue();
if (stringValue != null) {
String visitedString = resolveStringValue(stringValue);
typedStringValue.setValue(visitedString);
}
}
else if (value instanceof String) {
return resolveStringValue((String) value);
}
return value;
}
这里对占位符名称value
做了各种数据类型的处理,一般传入的占位符名称都是string
类型的,因此直接跳到resolveStringValue((String) value)
2.7 BeanDefinitionVisitor#resolveStringValue
@Nullable
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 original String if not modified.
return (strVal.equals(resolvedValue) ? strVal : resolvedValue);
}
这里使用了valueResolver.resolveStringValue(strVal)
进行解析,而追踪下来,我们知道valueResolver
其实就是2.2节中构造的解析器:
StringValueResolver valueResolver = strVal - > {
/* ignoreUnresolvablePlaceholders 是否无视不可解析的占位符,如果设置为false,那么碰到不可解析的占位符的时候,会抛出异常 */
String resolved = (this.ignoreUnresolvablePlaceholders ?
propertyResolver.resolvePlaceholders( strVal ) :
propertyResolver.resolveRequiredPlaceholders( strVal ) );
if ( this.trimValues )
{
resolved = resolved.trim();
}
return(resolved.equals( this.nullValue ) ? null : resolved);
};
那么,明显,resolveStringValue
方法也就是调用resolvePlaceholders
和resolveRequiredPlaceholders
中的其中一个,我们以更为严格的resolveRequiredPlaceholders
为例进行解析
2.8 AbstractPropertyResolver#resolveRequiredPlaceholders
@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
if (this.strictHelper == null) {
this.strictHelper = createPlaceholderHelper(false);
}
return doResolvePlaceholders(text, this.strictHelper);
}
可以看到是交由doResolvePlaceholders
去进行解析的
2.9 AbstractPropertyResolver#doResolvePlaceholders
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
// 注意,这里的形参PlaceholderResolver是一个函数式接口,这里传入getPropertyAsRawString方法作为PlaceholderResolver#resolvePlaceholder的实现
return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}
replacePlaceholders
方法的第二个参数是函数式接口参数PlaceholderResolver
:
@FunctionalInterface
public interface PlaceholderResolver {
/**
* Resolve the supplied placeholder name to the replacement value.
* @param placeholderName the name of the placeholder to resolve
* @return the replacement value, or {@code null} if no replacement is to be made
*/
@Nullable
String resolvePlaceholder(String placeholderName);
}
所以,getPropertyAsRawString
便是该函数式接口中resolvePlaceholder
的实现方法,而resolvePlaceholder
将会在2.11节中被调用
2.10 PropertyPlaceholderHelper#replacePlaceholders
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
return parseStringValue(value, placeholderResolver, null);
}
2.11 PropertyPlaceholderHelper#parseStringValue
protected String parseStringValue(
String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) {
// value:传入的原始占位符
// placeholderResolver:解析器
// visitedPlaceholders:缓存集合
int startIndex = value.indexOf(this.placeholderPrefix);
if (startIndex == -1) {
return value;
}
// spring允许存在多个占位符,如student.name可以配置为${student.name}${student.value}
// 所以下面的代码需要循环处理多个占位符,并对result进行替换
StringBuilder result = new StringBuilder(value);
while (startIndex != -1) {
// 查找结束符的位置
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
// 进行截取,去除占位符,得到真正的属性名称,比如${student.name} -> student.name
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (visitedPlaceholders == null) {
visitedPlaceholders = new HashSet<>(4);
}
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...
// 通过解析器获取到占位符对应的值
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
if (propVal == null && this.valueSeparator != null) {
// 获取不到值,说明此时placeholder可能具备默认值
// 查找默认值划分符号
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;
}
}
}
if (propVal != null) {
// Recursive invocation, parsing placeholders contained in the
// previously resolved placeholder value.
// 解析属性值
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) {
// Proceed with unprocessed value.
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();
}
方法看着很长,但逻辑其实挺好理解的,主要就是解决占位符嵌套和多个占位符这俩种情况,具体逻辑细看一下即可明白,不展开了,需要留意的是:
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
这里的resolvePlaceholder
实际上调用的是2.9节中的getPropertyAsRawString
方法
占位符内部嵌套的情况
2.12 PropertySourcesPropertyResolver#getPropertyAsRawString
@Nullable
protected String getPropertyAsRawString(String key) {
return getProperty(key, String.class, false);
}
@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
// 遍历,从各个propertySource中查找值,找到值之后马上返回,抛弃后面的propertySource
for (PropertySource<?> propertySource : this.propertySources) {
// 省略
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);
}
}
}
// 省略
return null;
}
核心逻辑就一句:
Object value = propertySource.getProperty(key);
从propertySource
取出元素值,不为空的话,直接返回,这也是2.1节中优先级的代码体现
3. 总结
配置文件的运作流程整个逻辑代码是挺好理解的,并不复杂,稍微花点心,就看懂了,逻辑总结起来不外乎几步:
- 获取配置文件,转化为
propertySources
- 构造基于
propertySources
的解析器valueResolver
- 获取
BeanDefinitions
,遍历,并对占位符逐一解析并替换
只不过在其中还穿插了比如占位符嵌套、值转化等场景的处理。
网友评论