「Spring源码分析」.properties文件读取及占位符$……替换源码

前言

我们在开发中常遇到一种场景,Bean里面有一些参数是比较固定的,这种时候通常会采用配置的方式,将这些参数配置在.properties文件中,然后在Bean实例化的时候通过Spring将这些.properties文件中配置的参数使用占位符"${...}"替换的方式读入并设置到Bean的相应参数中。

这种做法最典型的就是JDBC的配置,本文就来研究一下.properties文件读取及占位符"${}"替换的源码,首先从代码入手,定义一个DataSource,模拟一下JDBC四个参数:

1 public class DataSource {
 2 
 3 /**
 4 * 驱动类
 5 */
 6 private String driveClass;
 7 
 8 /**
 9 * jdbc地址
10 */
11 private String url;
12 
13 /**
14 * 用户名
15 */
16 private String userName;
17 
18 /**
19 * 密码
20 */
21 private String password;
22 
23 public String getDriveClass() {
24 return driveClass;
25 }
26 
27 public void setDriveClass(String driveClass) {
28 this.driveClass = driveClass;
29 }
30 
31 public String getUrl() {
32 return url;
33 }
34 
35 public void setUrl(String url) {
36 this.url = url;
37 }
38 
39 public String getUserName() {
40 return userName;
41 }
42 
43 public void setUserName(String userName) {
44 this.userName = userName;
45 }
46 
47 public String getPassword() {
48 return password;
49 }
50 
51 public void setPassword(String password) {
52 this.password = password;
53 }
54 
55 @Override
56 public String toString() {
57 return "DataSource [driveClass=" + driveClass + ", url=" + url + ", userName=" + userName + ", password=" + password + "]";
58 }
59 
60 }

定义一个db.properties文件:

1 driveClass=0
 2 url=1
 3 userName=2
 4 password=3

定义一个properties.xml文件:

1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4 xmlns:aop="http://www.springframework.org/schema/aop"
 5 xmlns:tx="http://www.springframework.org/schema/tx"
 6 xsi:schemaLocation="http://www.springframework.org/schema/beans
 7 http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
 8 http://www.springframework.org/schema/aop
 9 http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
10 
11 <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
12 <property name="location" value="properties/db.properties"></property>
13 </bean> 
14 
15 <bean id="dataSource" class="org.xrq.spring.action.properties.DataSource">
16 <property name="driveClass" value="${driveClass}" />
17 <property name="url" value="${url}" />
18 <property name="userName" value="${userName}" />
19 <property name="password" value="${password}" />
20 </bean>
21 
22 </beans>

写一段测试代码:

1 public class TestProperties {
 2 
 3 @Test
 4 public void testProperties() {
 5 ApplicationContext ac = new ClassPathXmlApplicationContext("spring/properties.xml");
 6 
 7 DataSource dataSource = (DataSource)ac.getBean("dataSource");
 8 System.out.println(dataSource);
 9 }
10 
11 }

运行结果就不贴了,很明显,下面就来分析一下Spring是如何将properties文件中的属性读入并替换"${}"占位符的。

PropertyPlaceholderConfigurer类解析

在properties.xml文件中我们看到了一个类PropertyPlaceholderConfigurer,顾名思义它就是一个属性占位符配置器,看一下这个类的继承关系图:

「Spring源码分析」.properties文件读取及占位符$……替换源码

看到从这张图上,我们能分析出来的最重要的一点就是PropertyPlaceholderConfigurer是BeanFactoryPostProcessor接口的实现类,想见Spring上下文必然是在Bean定义全部加载完毕后且Bean实例化之前通过postProcessBeanFactory方法一次性地替换了占位符"${}"

.properties文件读取源码解析

下面来看一下postProcessBeanFactory方法实现:

1 public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
 2 try {
 3 Properties mergedProps = mergeProperties();
 4 
 5 // Convert the merged properties, if necessary.
 6 convertProperties(mergedProps);
 7 
 8 // Let the subclass process the properties.
 9 processProperties(beanFactory, mergedProps);
10 }
11 catch (IOException ex) {
12 throw new BeanInitializationException("Could not load properties", ex);
13 }
14 }

跟一下第3行的mergeProperties方法:

1 protected Properties mergeProperties() throws IOException {
 2 Properties result = new Properties();
 3 
 4 if (this.localOverride) {
 5 // Load properties from file upfront, to let local properties override.
 6 loadProperties(result);
 7 }
 8 
 9 if (this.localProperties != null) {
10 for (Properties localProp : this.localProperties) {
11 CollectionUtils.mergePropertiesIntoMap(localProp, result);
12 }
13 }
14 
15 if (!this.localOverride) {
16 // Load properties from file afterwards, to let those properties override.
17 loadProperties(result);
18 }
19 
20 return result;
21 }

第2行的方法new出一个Properties,名为result,这个result会随着之后的代码传入,.properties文件中的数据会写入result中。

OK,接着看,代码进入第17行的方法,通过文件加载.properties文件:

1 protected void loadProperties(Properties props) throws IOException {
 2 if (this.locations != null) {
 3 for (Resource location : this.locations) {
 4 if (logger.isInfoEnabled()) {
 5 logger.info("Loading properties file from " + location);
 6 }
 7 InputStream is = null;
 8 try {
 9 is = location.getInputStream();
10 
11 String filename = null;
12 try {
13 filename = location.getFilename();
14 } catch (IllegalStateException ex) {
15 // resource is not file-based. See SPR-7552.
16 }
17 
18 if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {
19 this.propertiesPersister.loadFromXml(props, is);
20 }
21 else {
22 if (this.fileEncoding != null) {
23 this.propertiesPersister.load(props, new InputStreamReader(is, this.fileEncoding));
24 }
25 else {
26 this.propertiesPersister.load(props, is);
27 }
28 }
29 }
30 catch (IOException ex) {
31 if (this.ignoreResourceNotFound) {
32 if (logger.isWarnEnabled()) {
33 logger.warn("Could not load properties from " + location + ": " + ex.getMessage());
34 }
35 }
36 else {
37 throw ex;
38 }
39 }
40 finally {
41 if (is != null) {
42 is.close();
43 }
44 }
45 }
46 }
47 }

第9行,PropertyPlaceholderConfigurer的配置可以传入路径列表(当然这里只传了一个db.properties),第3行遍历列表,第9行通过一个输入字节流InputStream获取.properties对应的二进制数据,然后第23行的代码将InputStream中的二进制解析,写入第一个参数Properties中,Properties是JDK原生的读取.properties文件的工具。

就这样一个简单的流程,将.properties中的数据进行了解析,并写入result中(result是mergeProperties方法中new出的一个Properties)。

占位符"${...}"替换源码解析

上面看了.properties文件读取流程,接着就应当替换"${}"占位符了,还是回到postProcessBeanFactory方法:

1 public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
 2 try {
 3 Properties mergedProps = mergeProperties();
 4 
 5 // Convert the merged properties, if necessary.
 6 convertProperties(mergedProps);
 7 
 8 // Let the subclass process the properties.
 9 processProperties(beanFactory, mergedProps);
10 }
11 catch (IOException ex) {
12 throw new BeanInitializationException("Could not load properties", ex);
13 }
14 }

第3行合并了.properties文件(之所以叫做合并是因为多个.properties文件中可能有相同的Key)。

第6行在必要的情况下对合并的Properties进行转换,没看出有什么用。

第9行就开始替换占位符"${...}"了,要事先声明一点:BeanFactoryPostProcessor类的postProcessBeanFactory方法调用是在Bean定义解析之后,因此当前的beanFactory参数中已经有了所有的Bean定义,如果熟悉Bean解析流程的朋友对这一点应该很清楚。跟一下第9行的processProperties方法:

1 protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props)
 2 throws BeansException {
 3 
 4 StringValueResolver valueResolver = new PlaceholderResolvingStringValueResolver(props);
 5 BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
 6 
 7 String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
 8 for (String curName : beanNames) {
 9 // Check that we're not parsing our own bean definition,
10 // to avoid failing on unresolvable placeholders in properties file locations.
11 if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
12 BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
13 try {
14 visitor.visitBeanDefinition(bd);
15 }
16 catch (Exception ex) {
17 throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage());
18 }
19 }
20 }
21 
22 // New in Spring 2.5: resolve placeholders in alias target names and aliases as well.
23 beanFactoryToProcess.resolveAliases(valueResolver);
24 
25 // New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
26 beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
27 }

第4行new出一个PlaceholderResolvingStringValueResolver,传入Properties,顾名思义这是一个持有.properties文件配置的字符串值解析器。

第5行BeanDefinitionVistor,传入上面的StringValueResolver,顾名思义这是一个Bean定义访问工具,持有字符串值解析器,想见可以通过BeanDefinitionVistor访问Bean定义,在遇到需要解析的字符串的时候使用构造函数传入的StringValueResolver解析字符串

第7行通过BeanFactory获取所有Bean定义的名称。

第8行开始遍历所有Bean定义的名称,注意第11行的第一个判断"!(curName.equals(this.beanName)",this.beanName指的是PropertyPlaceholderConfigurer,意为PropertyPlaceholderConfigurer本身不会去解析占位符"${...}"。

着重跟14行的代码,BeanDefinitionVistor的visitBeanDefinition方法,传入BeanDefinition:

1 public void visitBeanDefinition(BeanDefinition beanDefinition) {
 2 visitParentName(beanDefinition);
 3 visitBeanClassName(beanDefinition);
 4 visitFactoryBeanName(beanDefinition);
 5 visitFactoryMethodName(beanDefinition);
 6 visitScope(beanDefinition);
 7 visitPropertyValues(beanDefinition.getPropertyValues());
 8 ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
 9 visitIndexedArgumentValues(cas.getIndexedArgumentValues());
10 visitGenericArgumentValues(cas.getGenericArgumentValues());
11 }

看到这个方法轮番访问<bean>定义中的parent、class、factory-bean、factory-method、scope、property、constructor-arg属性,但凡遇到需要"..."就进行解析。我们这里解析的是property标签中的"

..."就进行解析。我们这里解析的是property标签中的"{...}",因此跟一下第7行的代码:

1 protected void visitPropertyValues(MutablePropertyValues pvs) {
2 PropertyValue[] pvArray = pvs.getPropertyValues();
3 for (PropertyValue pv : pvArray) {
4 Object newVal = resolveValue(pv.getValue());
5 if (!ObjectUtils.nullSafeEquals(newVal, pv.getValue())) {
6 pvs.add(pv.getName(), newVal);
7 }
8 }
9 }

获取属性数组进行遍历,第4行的代码对属性值进行解析获取新属性值,第5行判断新属性值与原属性值不等,第6行的代码用新属性值替换原属性值。因此跟一下第4行的resolveValue方法:

1 protected Object resolveValue(Object value) {
 2 if (value instanceof BeanDefinition) {
 3 visitBeanDefinition((BeanDefinition) value);
 4 }
 5 else if (value instanceof BeanDefinitionHolder) {
 6 visitBeanDefinition(((BeanDefinitionHolder) value).getBeanDefinition());
 7 }
 8 else if (value instanceof RuntimeBeanReference) {
 9 RuntimeBeanReference ref = (RuntimeBeanReference) value;
10 String newBeanName = resolveStringValue(ref.getBeanName());
11 if (!newBeanName.equals(ref.getBeanName())) {
12 return new RuntimeBeanReference(newBeanName);
13 }
14 }
15 else if (value instanceof RuntimeBeanNameReference) {
16 RuntimeBeanNameReference ref = (RuntimeBeanNameReference) value;
17 String newBeanName = resolveStringValue(ref.getBeanName());
18 if (!newBeanName.equals(ref.getBeanName())) {
19 return new RuntimeBeanNameReference(newBeanName);
20 }
21 }
22 else if (value instanceof Object[]) {
23 visitArray((Object[]) value);
24 }
25 else if (value instanceof List) {
26 visitList((List) value);
27 }
28 else if (value instanceof Set) {
29 visitSet((Set) value);
30 }
31 else if (value instanceof Map) {
32 visitMap((Map) value);
33 }
34 else if (value instanceof TypedStringValue) {
35 TypedStringValue typedStringValue = (TypedStringValue) value;
36 String stringValue = typedStringValue.getValue();
37 if (stringValue != null) {
38 String visitedString = resolveStringValue(stringValue);
39 typedStringValue.setValue(visitedString);
40 }
41 }
42 else if (value instanceof String) {
43 return resolveStringValue((String) value);
44 }
45 return value;
46 }

这里主要对value类型做一个判断,我们配置文件里面配置的是字符串,因此就看字符串相关代码,即34行的判断进去,其余的差不多,可以自己看一下源码是怎么做的。第35~第36行的代码就是获取属性值,第38行的代码resolveStringValue方法解析字符串:

1 protected String resolveStringValue(String strVal) {
2 if (this.valueResolver == null) {
3 throw new IllegalStateException("No StringValueResolver specified - pass a resolver " +
4 "object into the constructor or override the 'resolveStringValue' method");
5 }
6 String resolvedValue = this.valueResolver.resolveStringValue(strVal);
7 // Return original String if not modified.
8 return (strVal.equals(resolvedValue) ? strVal : resolvedValue);
9 }

继续跟第6行的方法,valueResolver前面说过了,是传入的一个PlaceholderResolvingStringValueResolver,看一下resolveStringValue方法实现:

1 public String resolveStringValue(String strVal) throws BeansException {
 2 String value = this.helper.replacePlaceholders(strVal, this.resolver);
 3 return (value.equals(nullValue) ? null : value);
 4 }

第2行的replacePlaceholders方法顾名思义,替换占位符,它位于PropertyPlaceholderHelper类中,跟一下这个方法:

1 public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
 2 Assert.notNull(value, "Argument 'value' must not be null.");
 3 return parseStringValue(value, placeholderResolver, new HashSet<String>());
 4 }

继续跟第3行的parseStringValue方法,即追踪到了替换占位符的核心代码中:

1 protected String parseStringValue(
 2 String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
 3 
 4 StringBuilder buf = new StringBuilder(strVal);
 5 
 6 int startIndex = strVal.indexOf(this.placeholderPrefix);
 7 while (startIndex != -1) {
 8 int endIndex = findPlaceholderEndIndex(buf, startIndex);
 9 if (endIndex != -1) {
10 String placeholder = buf.substring(startIndex + this.placeholderPrefix.length(), endIndex);
11 if (!visitedPlaceholders.add(placeholder)) {
12 throw new IllegalArgumentException(
13 "Circular placeholder reference '" + placeholder + "' in property definitions");
14 }
15 // Recursive invocation, parsing placeholders contained in the placeholder key.
16 placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
17 
18 // Now obtain the value for the fully resolved key...
19 String propVal = placeholderResolver.resolvePlaceholder(placeholder);
20 if (propVal == null && this.valueSeparator != null) {
21 int separatorIndex = placeholder.indexOf(this.valueSeparator);
22 if (separatorIndex != -1) {
23 String actualPlaceholder = placeholder.substring(0, separatorIndex);
24 String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
25 propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
26 if (propVal == null) {
27 propVal = defaultValue;
28 }
29 }
30 }
31 if (propVal != null) {
32 // Recursive invocation, parsing placeholders contained in the
33 // previously resolved placeholder value.
34 propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
35 buf.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
36 if (logger.isTraceEnabled()) {
37 logger.trace("Resolved placeholder '" + placeholder + "'");
38 }
39 startIndex = buf.indexOf(this.placeholderPrefix, startIndex + propVal.length());
40 }
41 else if (this.ignoreUnresolvablePlaceholders) {
42 // Proceed with unprocessed value.
43 startIndex = buf.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
44 }
45 else {
46 throw new IllegalArgumentException("Could not resolve placeholder '" + placeholder + "'");
47 }
48 
49 visitedPlaceholders.remove(placeholder);
50 }
51 else {
52 startIndex = -1;
53 }
54 }
55 
56 return buf.toString();
57 }

过一下此流程:

  1. 获取占位符前缀"${"的位置索引startIndex
  2. 占位符前缀"{"存在,从"{"存在,从"{"后面开始获取占位符后缀"}"的位置索引endIndex
  3. 如果占位符前缀位置索引startIndex与占位符后缀的位置索引endIndex都存在,截取中间的部分placeHolder
  4. 从Properties中获取placeHolder对应的值propVal
  5. 如果propVal不存在,尝试对placeHolder使用":"进行一次分割,如果分割出来有结果,那么前面一部分命名为actualPlaceholder,后面一部分命名为defaultValue,尝试从Properties中获取actualPlaceholder对应的value,如果存在则取此value,如果不存在则取defaultValue,最终赋值给propVal
  6. 返回propVal,就是替换之后的值

流程很长,通过这样一整个的流程,将占位符"${...}"中的内容替换为了我们需要的值。

「Spring源码分析」.properties文件读取及占位符$……替换源码

相关推荐