前言
我們在開發中常遇到一種場景,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 }
過一下此流程:
- 獲取佔位符字首"${"的位置索引startIndex
- 佔位符字首"${"存在,從"${"後面開始獲取佔位符字尾"}"的位置索引endIndex
- 如果佔位符字首位置索引startIndex與佔位符字尾的位置索引endIndex都存在,擷取中間的部分placeHolder
- 從Properties中獲取placeHolder對應的值propVal
- 如果propVal不存在,嘗試對placeHolder使用":"進行一次分割,如果分割出來有結果,那麼前面一部分命名為actualPlaceholder,後面一部分命名為defaultValue,嘗試從Properties中獲取actualPlaceholder對應的value,如果存在則取此value,如果不存在則取defaultValue,最終賦值給propVal
- 返回propVal,就是替換之後的值
流程很長,通過這樣一整個的流程,將佔位符"${...}"中的內容替換為了我們需要的值。