【Spring原始碼分析】.properties檔案讀取及佔位符${...}替換原始碼解析

五月的倉頡發表於2017-04-30

前言

我們在開發中常遇到一種場景,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,顧名思義它就是一個屬性佔位符配置器,看一下這個類的繼承關係圖:

看到從這張圖上,我們能分析出來的最重要的一點就是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標籤中的"${...}",因此跟一下第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,就是替換之後的值

流程很長,通過這樣一整個的流程,將佔位符"${...}"中的內容替換為了我們需要的值。

相關文章