前言
我們在開發中常遇到一種場景,Bean里面有一些參數是比較固定的,這種時候通常會采用配置的方式,將這些參數配置在.properties文件中,然后在Bean實例化的時候通過Spring將這些.properties文件中配置的參數使用占位符"${}"替換的方式讀入并設置到Bean的相應參數中。
這種做法最典型的就是JDBC的配置,本文就來研究一下.properties文件讀取及占位符"${}"替換的源碼,首先從代碼入手,定義一個DataSource,模擬一下JDBC四個參數:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
public class DataSource { /** * 驅動類 */ private String driveClass; /** * jdbc地址 */ private String url; /** * 用戶名 */ private String userName; /** * 密碼 */ private String password; public String getDriveClass() { return driveClass; } public void setDriveClass(String driveClass) { this .driveClass = driveClass; } public String getUrl() { return url; } public void setUrl(String url) { this .url = url; } public String getUserName() { return userName; } public void setUserName(String userName) { this .userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this .password = password; } @Override public String toString() { return "DataSource [driveClass=" + driveClass + ", url=" + url + ", userName=" + userName + ", password=" + password + "]" ; } } |
定義一個db.properties文件:
1
2
3
4
|
driveClass= 0 url= 1 userName= 2 password= 3 |
定義一個properties.xml文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<? xml version = "1.0" encoding = "UTF-8" ?> < beans xmlns = "http://www.springframework.org/schema/beans" xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" xmlns:aop = "http://www.springframework.org/schema/aop" xmlns:tx = "http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> < bean class = "org.springframework.beans.factory.config.PropertyPlaceholderConfigurer" > < property name = "location" value = "properties/db.properties" ></ property > </ bean > < bean id = "dataSource" class = "org.xrq.spring.action.properties.DataSource" > < property name = "driveClass" value = "${driveClass}" /> < property name = "url" value = "${url}" /> < property name = "userName" value = "${userName}" /> < property name = "password" value = "${password}" /> </ bean > </ beans > |
寫一段測試代碼:
1
2
3
4
5
6
7
8
9
10
|
public class TestProperties { @Test public void testProperties() { ApplicationContext ac = new ClassPathXmlApplicationContext( "spring/properties.xml" ); DataSource dataSource = (DataSource)ac.getBean( "dataSource" ); System.out.println(dataSource); } } |
運行結果就不貼了,很明顯,下面就來分析一下Spring是如何將properties文件中的屬性讀入并替換"${}"占位符的。
PropertyPlaceholderConfigurer類解析
在properties.xml文件中我們看到了一個類PropertyPlaceholderConfigurer,顧名思義它就是一個屬性占位符配置器,看一下這個類的繼承關系圖:
看到從這張圖上,我們能分析出來的最重要的一點就是PropertyPlaceholderConfigurer是BeanFactoryPostProcessor接口的實現類,想見Spring上下文必然是在Bean定義全部加載完畢后且Bean實例化之前通過postProcessBeanFactory方法一次性地替換了占位符"${}"。
.properties文件讀取源碼解析
下面來看一下postProcessBeanFactory方法實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { try { Properties mergedProps = mergeProperties(); // Convert the merged properties, if necessary. convertProperties(mergedProps); // Let the subclass process the properties. processProperties(beanFactory, mergedProps); } catch (IOException ex) { throw new BeanInitializationException( "Could not load properties" , ex); } } |
跟一下第3行的mergeProperties方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
protected Properties mergeProperties() throws IOException { Properties result = new Properties(); if ( this .localOverride) { // Load properties from file upfront, to let local properties override. loadProperties(result); } if ( this .localProperties != null ) { for (Properties localProp : this .localProperties) { CollectionUtils.mergePropertiesIntoMap(localProp, result); } } if (! this .localOverride) { // Load properties from file afterwards, to let those properties override. loadProperties(result); } return result; } |
第2行的方法new出一個Properties,名為result,這個result會隨著之后的代碼傳入,.properties文件中的數據會寫入result中。
OK,接著看,代碼進入第17行的方法,通過文件加載.properties文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
protected void loadProperties(Properties props) throws IOException { if ( this .locations != null ) { for (Resource location : this .locations) { if (logger.isInfoEnabled()) { logger.info( "Loading properties file from " + location); } InputStream is = null ; try { is = location.getInputStream(); String filename = null ; try { filename = location.getFilename(); } catch (IllegalStateException ex) { // resource is not file-based. See SPR-7552. } if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) { this .propertiesPersister.loadFromXml(props, is); } else { if ( this .fileEncoding != null ) { this .propertiesPersister.load(props, new InputStreamReader(is, this .fileEncoding)); } else { this .propertiesPersister.load(props, is); } } } catch (IOException ex) { if ( this .ignoreResourceNotFound) { if (logger.isWarnEnabled()) { logger.warn( "Could not load properties from " + location + ": " + ex.getMessage()); } } else { throw ex; } } finally { if (is != null ) { is.close(); } } } } } |
第9行,PropertyPlaceholderConfigurer的配置可以傳入路徑列表(當然這里只傳了一個db.properties),第3行遍歷列表,第9行通過一個輸入字節流InputStream獲取.properties對應的二進制數據,然后第23行的代碼將InputStream中的二進制解析,寫入第一個參數Properties中,Properties是JDK原生的讀取.properties文件的工具。
就這樣一個簡單的流程,將.properties中的數據進行了解析,并寫入result中(result是mergeProperties方法中new出的一個Properties)。
占位符"${...}"替換源碼解析
上面看了.properties文件讀取流程,接著就應當替換"${}"占位符了,還是回到postProcessBeanFactory方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { try { Properties mergedProps = mergeProperties(); // Convert the merged properties, if necessary. convertProperties(mergedProps); // Let the subclass process the properties. processProperties(beanFactory, mergedProps); } catch (IOException ex) { throw new BeanInitializationException( "Could not load properties" , ex); } } |
第3行合并了.properties文件(之所以叫做合并是因為多個.properties文件中可能有相同的Key)。
第6行在必要的情況下對合并的Properties進行轉換,沒看出有什么用。
第9行就開始替換占位符"${...}"了,要事先聲明一點:BeanFactoryPostProcessor類的postProcessBeanFactory方法調用是在Bean定義解析之后,因此當前的beanFactory參數中已經有了所有的Bean定義,如果熟悉Bean解析流程的朋友對這一點應該很清楚。跟一下第9行的processProperties方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException { StringValueResolver valueResolver = new PlaceholderResolvingStringValueResolver(props); BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver); 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. if (!(curName.equals( this .beanName) && beanFactoryToProcess.equals( this .beanFactory))) { BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName); try { visitor.visitBeanDefinition(bd); } catch (Exception ex) { throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage()); } } } // 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. beanFactoryToProcess.addEmbeddedValueResolver(valueResolver); } |
第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
2
3
4
5
6
7
8
9
10
11
|
public void visitBeanDefinition(BeanDefinition beanDefinition) { visitParentName(beanDefinition); visitBeanClassName(beanDefinition); visitFactoryBeanName(beanDefinition); visitFactoryMethodName(beanDefinition); visitScope(beanDefinition); visitPropertyValues(beanDefinition.getPropertyValues()); ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues(); visitIndexedArgumentValues(cas.getIndexedArgumentValues()); visitGenericArgumentValues(cas.getGenericArgumentValues()); } |
看到這個方法輪番訪問<bean>定義中的parent、class、factory-bean、factory-method、scope、property、constructor-arg屬性,但凡遇到需要"${...}"就進行解析。我們這里解析的是property標簽中的"${...}",因此跟一下第7行的代碼:
1
2
3
4
5
6
7
8
9
|
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); } } } |
獲取屬性數組進行遍歷,第4行的代碼對屬性值進行解析獲取新屬性值,第5行判斷新屬性值與原屬性值不等,第6行的代碼用新屬性值替換原屬性值。因此跟一下第4行的resolveValue方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
protected Object resolveValue(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.equals(ref.getBeanName())) { return new RuntimeBeanReference(newBeanName); } } else if (value instanceof RuntimeBeanNameReference) { RuntimeBeanNameReference ref = (RuntimeBeanNameReference) value; String newBeanName = resolveStringValue(ref.getBeanName()); 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類型做一個判斷,我們配置文件里面配置的是字符串,因此就看字符串相關代碼,即34行的判斷進去,其余的差不多,可以自己看一下源碼是怎么做的。第35~第36行的代碼就是獲取屬性值,第38行的代碼resolveStringValue方法解析字符串:
1
2
3
4
5
6
7
8
9
|
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); } |
繼續跟第6行的方法,valueResolver前面說過了,是傳入的一個PlaceholderResolvingStringValueResolver,看一下resolveStringValue方法實現:
1
2
3
4
|
public String resolveStringValue(String strVal) throws BeansException { String value = this .helper.replacePlaceholders(strVal, this .resolver); return (value.equals(nullValue) ? null : value); } |
第2行的replacePlaceholders方法顧名思義,替換占位符,它位于PropertyPlaceholderHelper類中,跟一下這個方法:
1
2
3
4
|
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) { Assert.notNull(value, "Argument 'value' must not be null." ); return parseStringValue(value, placeholderResolver, new HashSet<String>()); } |
繼續跟第3行的parseStringValue方法,即追蹤到了替換占位符的核心代碼中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
protected String parseStringValue( String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) { StringBuilder buf = new StringBuilder(strVal); int startIndex = strVal.indexOf( this .placeholderPrefix); while (startIndex != - 1 ) { int endIndex = findPlaceholderEndIndex(buf, startIndex); if (endIndex != - 1 ) { String placeholder = buf.substring(startIndex + this .placeholderPrefix.length(), endIndex); if (!visitedPlaceholders.add(placeholder)) { throw new IllegalArgumentException( "Circular placeholder reference '" + placeholder + "' 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 ) { 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); buf.replace(startIndex, endIndex + this .placeholderSuffix.length(), propVal); if (logger.isTraceEnabled()) { logger.trace( "Resolved placeholder '" + placeholder + "'" ); } startIndex = buf.indexOf( this .placeholderPrefix, startIndex + propVal.length()); } else if ( this .ignoreUnresolvablePlaceholders) { // Proceed with unprocessed value. startIndex = buf.indexOf( this .placeholderPrefix, endIndex + this .placeholderSuffix.length()); } else { throw new IllegalArgumentException( "Could not resolve placeholder '" + placeholder + "'" ); } visitedPlaceholders.remove(placeholder); } else { startIndex = - 1 ; } } return buf.toString(); } |
過一下此流程:
- 獲取占位符前綴"${"的位置索引startIndex
- 占位符前綴"${"存在,從"${"后面開始獲取占位符后綴"}"的位置索引endIndex
- 如果占位符前綴位置索引startIndex與占位符后綴的位置索引endIndex都存在,截取中間的部分placeHolder
- 從Properties中獲取placeHolder對應的值propVal
- 如果propVal不存在,嘗試對placeHolder使用":"進行一次分割,如果分割出來有結果,那么前面一部分命名為actualPlaceholder,后面一部分命名為defaultValue,嘗試從Properties中獲取actualPlaceholder對應的value,如果存在則取此value,如果不存在則取defaultValue,最終賦值給propVal
- 返回propVal,就是替換之后的值
流程很長,通過這樣一整個的流程,將占位符"${...}"中的內容替換為了我們需要的值。
以上就是本文的全部內容,希望本文的內容對大家的學習或者工作能帶來一定的幫助,同時也希望多多支持服務器之家!
原文鏈接:http://www.cnblogs.com/xrq730/p/6785473.html