.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()); }
看到这个方*番访问<bean>定义中的parent、class、factory-bean、factory-method、scope、property、constructor-arg属性,但凡遇到需要"${...}"就进行解析。我们这里解析的是property标签中的"${...}",因此跟一下第7行的代码:
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,就是替换之后的值
流程很长,通过这样一整个的流程,将占位符"${...}"中的内容替换为了我们需要的值。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!
下一篇: Yii编程开发常见调用技巧集锦