本篇文章主要介紹了.properties檔案讀取及佔位符${...}替換原始碼解析的相關知識,具有很好的參考價值。下面跟著小編一起來看下吧
前言
我們在開發中常遇到一種場景,Bean裡面有一些參數是比較固定的,這種時候通常會採用配置的方式,將這些參數配置在.properties檔案中,然後在Bean實例化的時候透過Spring將這些.properties檔案中配置的參數使用佔位符"${}"替換的方式讀入並設定到Bean的相應參數中。
這種做法最典型的就是JDBC的配置,本文就來研究一下.properties檔案讀取及佔位符"${}"替換的源碼,先從程式碼入手,定義一個DataSource,模擬一下JDBC四個參數:
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檔案:
driveClass=0 url=1 userName=2 password=3
定義一個properties.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" 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>
寫一段測試程式碼:
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方法實作:
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方法:
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檔:
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方法:
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方法:
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:
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()); }
看到这个方法轮番访问
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方法:
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方法解析字符串:
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方法实现:
public String resolveStringValue(String strVal) throws BeansException { String value = this.helper.replacePlaceholders(strVal, this.resolver); return (value.equals(nullValue) ? null : value); }
第2行的replacePlaceholders方法顾名思义,替换占位符,它位于PropertyPlaceholderHelper类中,跟一下这个方法:
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方法,即追踪到了替换占位符的核心代码中:
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,就是替换之后的值
流程很长,通过这样一整个的流程,将占位符"${...}"中的内容替换为了我们需要的值。
【相关推荐】
1. Java免费视频教程
2. JAVA教程手册
3. 全面解析Java注解
以上是讀取.properties和占位符${...}替換源碼的方法介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本文討論了使用Maven和Gradle進行Java項目管理,構建自動化和依賴性解決方案,以比較其方法和優化策略。

本文使用Maven和Gradle之類的工具討論了具有適當的版本控制和依賴關係管理的自定義Java庫(JAR文件)的創建和使用。

本文討論了使用咖啡因和Guava緩存在Java中實施多層緩存以提高應用程序性能。它涵蓋設置,集成和績效優勢,以及配置和驅逐政策管理最佳PRA

本文討論了使用JPA進行對象相關映射,並具有高級功能,例如緩存和懶惰加載。它涵蓋了設置,實體映射和優化性能的最佳實踐,同時突出潛在的陷阱。[159個字符]

Java的類上載涉及使用帶有引導,擴展程序和應用程序類負載器的分層系統加載,鏈接和初始化類。父代授權模型確保首先加載核心類別,從而影響自定義類LOA


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

禪工作室 13.0.1
強大的PHP整合開發環境

WebStorm Mac版
好用的JavaScript開發工具

MantisBT
Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

SublimeText3 Linux新版
SublimeText3 Linux最新版

記事本++7.3.1
好用且免費的程式碼編輯器