1.背景
在开发的过程中,我们通常会使用ini、xml、json等配置文件对某些服务应用的参数进行配置,这些包含各层级结构的配置文件,大致可以看作树状结构,其解析和拼装并不是一项简单的事情。
在本项目中,开发人员或者业务人员提供了这些配置文件之后,需要解析出相应的配置项以及其值,每一项配置都以一条记录的形式保存到数据库中。服务应用以一定的周期对数据库中的配置项进行读取并拼装,以便刷新本地的配置文件。
整个业务流程的简要示意图如Fig. 1所示:

本文所涉及的内容,就是后台管理服务器对配置文件的解析,以及应用服务器对配置文件的拼装。
2.实现方案
从Fig. 1的业务流程中可以看出,整个系统需要实现的两个关键功能包括:配置文件的解析和配置文件的拼装。在拼装的时候我们需要合适的数据结构去承载这些配置项记录。
2.1配置项的数据结构
在本项目中只包含两种格式的配置文件,一种是ini格式,另一种是xml格式。考虑到每一种配置文件的最小粒度就是一项配置,我们将每一项配置作为一条记录,存放于数据库中。
数据库表的每一条记录中,关键字段包含配置节点名(或者说配置项名)、配置节点索引、配置节点值,这三项关系到一个配置文件的解析(拆分)和拼装,如果缺少其一,配置文件是无法解析和拼装的。
当然,这些文件在数据库中保存时所需要的字段不止这些,例如文件名、服务应用名等,这里为了简化模型,突出本文所要描述的技术点,仅给出三个主要字段。
2.1.1.ini格式的配置文件
ini的配置文件格式通常如下所示:
[Section1]
key1=value1
key2=value2
[Section2]
key1=value1
key2=value2
保存到数据库表中对应的结构如下表Tab. 1所示:
ConfNodeName | ConfNodeValue | ConfNodeIndex |
Section1 | 0 | |
Section1|key1 | value1 | 0|0 |
Section1|key2 | value2 | 0|1 |
Section2 | 1 | |
Section2|key1 | value1 | 1|0 |
Section2|key2 | value2 | 1|1 |
其中ConfNodeName为完整的配置节点名,ConfNodeValue为节点的值,ConfNodeIndex为该节点在当前配置文件中的索引(或者说位置)。
比如说Section1是一个父节点,其值为空,其位置是在第1个层级的第0个位置(编号以0开始),那么ConfNodeIndex为0;在Section1中的节点key1,其ConfNodeName为Section|key1,由于该节点是叶节点(没有子节点),那么该节点是有值的,其对应的ConfNodeValue为value1,该节点的位置在第0个节点下的第0个位置,该节点的ConfNodeIndex为0|0;其他节点依次类推。
2.1.2.xml格式的配置文件
xml的配置文件格式通常如下所示:
<?xml version="1.0" encoding="utf-8"?>
<Item ID="3.14159" Title="清仓大甩卖" CorpNo="666666" ShowTopBar="true">
<Src>https://www.xxxxxx.com/xxx/wap/enterAction!enter.ac?service=h5_app</Src>
<CorpName>OhYeah</CorpName>
<FuncType>1</FuncType>
<Deskey>test1</Deskey>
<ClientKeyIDUrl AuthName="XCooperation">/OutterStore/OS_GetAuthToken.aspx</ClientKeyIDUrl>
</Item>
保存成到数据库表中对应的结构如下表Tab. 2所示:
ConfNodeName | ConfNodeValue | ConfNodeIndex |
Item | 0 | |
Item|ID | 3.14159 | 0 |
Item|Title | 清仓大甩卖 | 0 |
Item|CorpNo | 666666 | 0 |
Item|ShowTopBar | true | 0 |
Item|Src | https://www.xxxxxx.com/xxx/wap/enterAction!enter.ac?service=h5_app | 0|0 |
Item|CorpName | OhYeah | 0|1 |
Item|FuncType | 1 | 0|2 |
Item|Deskey | test1 | 0|3 |
Item|ClientKeyIDUrl | /OutterStore/OS_GetAuthToken.aspx | 0|4 |
Item|ClientKeyIDUrl|AuthName | XCooperation | 0|4 |
对于xml的存储格式,相对比ini格式复杂,尽管两者都可以看成是树状结构的文件。xml文件相对比较复杂的原因主要有:
1). xml文件中节点的层数可能会存在两级以上,而ini文件中节点只有两级。我们在寻找节点之间的父子关系时,层级越多,难度越大。
2). xml文件中节点可能会存在属性项。比如Item节点中就包含了ID、Title、CorpNo、ShowTopBar共4项属性,这些属性与所属的节点处于同一个层级,有着相同的ConfNodeIndex,只是在ConfNodeName中,多了一道竖杠“|”划分层级,以表示所属的节点。因此我们在拼装配置文件时,从数据库中读取了属于同一个文件的每条配置项记录之后,需要区分这条记录保存的到底是一个节点,还是一个节点的属性。区分方法也不难,只要在ConfNodeName中竖杠“|”的数量和ConfNodeIndex中的一样,该条记录就是节点;如果ConfNodeName中竖杠“|”的数量比ConfNodeIndex的多1个,那么该条记录保存的就是一项属性;其他情况?不存在的,只能报错。
为了更形象生动地描述其树状结构,这里给出这个xml文件的树状结构图:

2.1.3.用于保存配置项记录的类
在与web端进行socket通信时,我们的报文是json格式的,其中内嵌了配置文件的信息。为方便我们进行解析和拼装,以及对数据库的存储,我们创建一个类,每一个配置项保存为该类的一个对象。
这个类包含了ConfNodeName、ConfNodeValue、ConfNodeIndex三个成员变量,我们只需要使用gson等工具包即可进行请求报文的序列化和反序列化,也就是说,我们可以将保存着配置项信息的对象转换成一个json格式的报文,也可以将json报文中的配置文件转换成对象。该配置项类的定义如下:
public class CItem{
private String ConfNodeName;
private String ConfNodeValue;
private String ConfNodeIndex;
public String getConfNodeName(){return this.ConfNodeName;}
public void setConfNodeName(String val){this.ConfNodeName=val;}
public String getConfNodeValue(){return this.ConfNodeValue;}
public void setConfNodeValue(String val){this.ConfNodeValue=val;}
public String getConfNodeIndex(){return this.ConfNodeIndex;}
public void setConfNodeIndex(String val){this.ConfNodeIndex=val;}
}
上面的类中,实际上远不止这些成员变量,有的东西不便透露,咱只纯粹讨论解析和拼装的技术问题,这三个变量够用了。
2.2.配置文件的解析
在清楚了配置文件的数据结构之后,无论是解析还是拼装配置文件,都会有比较明确的目标。对配置文件的解析过程,实质上就是将ini或者xml格式的文件解析出配置项数据,并将这些配置项数据保存于配置项类CItem中。
2.2.1.ini配置文件的解析
解析ini配置文件分成两个部分,第一部分是将ini格式String转化成Map<String, Map<String, String>>类型的{Section名}-{key-value对映射表}映射表,可能有点拗口,但是不难理解。第二部分将这个映射表作为输入,最终转化成List<CItem>类型输出。
第一部分流程图如图所示。

转化成一个映射表之后,最后要转化成List<CItem>类型输出,其过程如图Fig. 4所示。

ini文件解析过程的代码实现如下:
public class ParseIniUtil {
public ParseIniUtil(){};
public static Map<String, Map<String, String>> doParse(String strFile) throws Exception {
Map<String,String> mapTemp = null;
StringTokenizer stkFile = new StringTokenizer(strFile, "\r\n");
Map<String, Map<String, String>> mapSection = new LinkedHashMap<String, Map<String, String>>();
while(stkFile.hasMoreTokens()) {
String strLine = stkFile.nextToken().trim();
char ch = strLine.charAt(0);
if(ch != 59 && ch != 35 && ch != 33) {
if(ch == 91) {
String idx = strLine.substring(1, strLine.length() - 1).trim();
mapTemp = new LinkedHashMap<String,String>();
mapSection.put(idx, mapTemp);
} else {
int idx1 = strLine.indexOf("=");
if(idx1 == -1) {
throw new Exception("Ini: no \'=\'");
}
String strKey = strLine.substring(0, idx1);
String strValue = strLine.substring(idx1 + 1);
mapTemp.put(strKey, strValue);
}
}
}
return mapSection;
}
public static List<CItem> parseIni(String strIni) throws Exception {
List<CItem> lsRs = new ArrayList<CItem>();
Map<String, Map<String, String>> mapSection = doParse(strIni);
if ( null != mapSection && 0 < mapSection.size() ) {
Iterator<String> iterParent = mapSection.keySet().iterator();
int iRootIndex = 0;
while (iterParent.hasNext()) {
String strModuleKey = String.valueOf(iterParent.next());
Map<String,String> mapModuleVal = mapSection.get(strModuleKey);
CItem clsRootBp = new CItem();
clsRootBp.setConfNodeName(strModuleKey);
clsRootBp.setConfNodeIndex("" + iRootIndex);
lsRs.add(clsRootBp);
Iterator<String> iterChild = mapModuleVal.keySet().iterator();
int iChildIndex = 0;
while(iterChild.hasNext()){
String strParamKey = String.valueOf(iterChild.next());
String strParamValue = mapModuleVal.get(strParamKey);
CItem clsChildBp = new CItem();
clsChildBp.setConfNodeName(strModuleKey+"|"+strParamKey);
clsChildBp.setConfNodeValue(strParamValue);
clsChildBp.setConfNodeIndex( iRootIndex + "|" + iChildIndex );
lsRs.add(clsChildBp);
iChildIndex++;
}
iRootIndex++;
}
}
return lsRs;
}
}
2.2.2.xml配置文件的解析
根据配置节点排序的规则(详情参照3.3.1.1),xml文件解析流程如图所示

该过程大致的中心思想就是,(1)顺着输入的xml字符串先找<element>头并添加到输出结果列表,(2)然后如果该element有属性就添加属性到列表,(3)有子节点就递归添加子节点,没有子节点就直接设置当前element的value,(4)找到当前节点的结尾</element>,让输入的xml字串等于</element>后面的子串,(4)继续循环,直到xml字串长度为0,或者再也找不到<xxx>字样的字符串,结束循环并输出结果。
对应的代码如下:
public class ParseXmlUtil {
public ParseXmlUtil(){}
//递归地调用,将xml字符串解析成一个对象列表
public static List<CItem> xmlStrToList(String strSource, String strParentName, String strParentIndex) {
if(strSource == null) {
return null;
}
List<CItem> lsBp = new ArrayList<CItem>();
int iCurrentNo = 0;
while(strSource.length()>0) {
int iStartPos = strSource.indexOf(60); // 60 '<'
if(iStartPos == -1) {
break;
}
int iEndPos = strSource.indexOf(62, iStartPos); //62 '>'
if(iEndPos == -1) {
break;
}
//如果是<!...>类型,则不将此作为一个元素
char cFirstChar = strSource.charAt(iStartPos + 1);
if(33 == cFirstChar || 63 == cFirstChar) { //33 '!'; 63 '?'
strSource = strSource.substring(iEndPos);
continue;
} else {
String strElemName = strSource.substring(iStartPos + 1, iEndPos).trim();
String strConfNodeName;
String strConfNodeIndex;
int iSpacePos = strElemName.indexOf(" ");
CItem clsBpElement = new CItem();
List<CItem> lsChildBp = new ArrayList<CItem>(); // 用于保存子串的元素列表
//如果元素包含属性,那么元素的名字在第一个空格处结尾
if(-1 != iSpacePos){
String[] arrStrProperty = strElemName.split(" ");
int iArrLen = arrStrProperty.length;
//设置节点名称以及值
strConfNodeName = (null == strParentName || 0 == strParentName.length())?(arrStrProperty[0]):(strParentName+"|"+arrStrProperty[0]);
strConfNodeIndex = (null == strParentIndex || 0 == strParentIndex.length())? (""+iCurrentNo):(strParentIndex+"|"+iCurrentNo);
clsBpElement.setConfNodeName(strConfNodeName);
clsBpElement.setConfNodeIndex(strConfNodeIndex);
//对于节点是否有子节点,仍需进一步判断,如果没有,就设置其值
String strEndTag = "</"+arrStrProperty[0]+">";
int iChildStrEnd = strSource.indexOf(strEndTag);
int iChildStrStart = iEndPos + 1;
//如果没有子串,或者说子串的长度为0,就既不用设置节点值,也不用递归调用子串方法
if(iChildStrEnd > iChildStrStart){
String strChildStr = strSource.substring(iChildStrStart, iChildStrEnd).trim();
int iBracketPos = strChildStr.indexOf("<");
//如果节点子串不包含有尖括号,即节点不包含有子节点
if(-1 == iBracketPos){
clsBpElement.setConfNodeValue(strChildStr.trim());
}else{
//递归调用:当前节点的子串递归
lsChildBp = xmlStrToList(strChildStr,strConfNodeName,strConfNodeIndex);
}
}
lsBp.add(clsBpElement);
//设置节点的属性
for(int i = 1; i<iArrLen; i++) {
int iKeyEndPos = arrStrProperty[i].indexOf("=");
if(-1 == iKeyEndPos){
continue;
}
String strPropName = arrStrProperty[i].substring(0,iKeyEndPos).trim();
int iValStartPos = iKeyEndPos + 1;
//对于属性,不仅要去除前后的空格,还要去除两端的引号
String strPropVal = arrStrProperty[i].substring(iValStartPos).trim().replace("\"", "");
//创建属性并添加到列表
CItem clsBpProp = new CItem();
clsBpProp.setConfNodeName(strConfNodeName+"|"+strPropName);
clsBpProp.setConfNodeIndex(strConfNodeIndex);
clsBpProp.setConfNodeValue(strPropVal);
lsBp.add(clsBpProp);
}
int iEndTagLen = strEndTag.length();
int iEndTagPos = iChildStrEnd;
strElemName = arrStrProperty[0];
//将当前子串截断到当前节点末尾处
strSource = strSource.substring(iEndTagPos+iEndTagLen).trim();
}else{
//设置节点名称以及值
strConfNodeName = (null == strParentName || 0 == strParentName.length())?(strElemName):(strParentName+"|"+strElemName);
strConfNodeIndex = (null == strParentIndex || 0 == strParentIndex.length())? (""+iCurrentNo):(strParentIndex+"|"+iCurrentNo);
clsBpElement.setConfNodeName(strConfNodeName);
clsBpElement.setConfNodeIndex(strConfNodeIndex);
//对于节点是否有子节点,仍需进一步判断,如果没有,就设置其值
int iChildStrEnd = strSource.indexOf("</"+strElemName+">");
int iChildStrStart = iEndPos + 1;
//如果没有子串,或者说子串的长度为0,就既不用设置节点值,也不用递归调用子串方法
if(iChildStrEnd > iChildStrStart){
String strChildStr = strSource.substring(iChildStrStart, iChildStrEnd);
int iBracketPos = strChildStr.indexOf("<");
//如果节点不包含有尖括号,即不包含有子节点
if(-1 == iBracketPos){
clsBpElement.setConfNodeValue(strChildStr.trim());
}else{
//递归调用:当前节点的子串递归
lsChildBp = xmlStrToList(strChildStr,strConfNodeName,strConfNodeIndex);
}
}
lsBp.add(clsBpElement);
}
//将当前节点子串递归的返回值进行拼接
if(null != lsChildBp && lsChildBp.size() > 0) {
lsBp.addAll(lsChildBp);
}
//结束条件很重要!!!
String strEndXmlTag = "</" + strElemName + ">";
int iEndTagPos = strSource.indexOf(strEndXmlTag);
if(iEndTagPos == -1) {
break;
}
//lsBp.addAll(xmlStrToList(strSource.substring(iEndTagPos+strEndXmlTag.length()),strConfNodeName,strConfNodeIndex));
//将当前子串截断到当前节点末尾处
strSource = strSource.substring(iEndTagPos+strEndXmlTag.length()).trim();
}
iCurrentNo++;
}
return lsBp;
}
}
解析和拼装是互逆的过程,可以在看完拼装之后回过头来看解析进行对比,可以加深理解
2.3.配置文件的拼装
2.3.1配置节点森林的建立
顾名思义,森林是由多棵树组成,也就是说一个配置文件里面可能存在多个配置根节点,一个根节点就可以表示一棵树。例如2.1.1中的ini配置文件,每个Section就是一个根节点,底下的若干个key-value对就是其子节点和子节点的值。
2.3.1.1.节点的数据结构
在拼装配置文件时,由于配置文件的配置项节点可以看做是树状结构存储的,如图Fig. 2所示。为了建立起节点之间的父子关系,咱创建一个节点ConfNode类。这里必须要注意的是,CItem类和这里的ConfNode节点类是有区别的,区别在于:
- 前者用于存储从数据库里读取回来的记录,因此CItem对象有可能是一个节点,也有是一项属性;而ConfNode只表示一个节点,该节点的属性(如果有)保存于其Map<String, String>类型的成员变量property中;
- 前者的数据结构中没有直接体现节点之间父子关系的成员变量,只能通过ConfNodeIndex和ConfNodeName寻找父子关系,而后者ConfNode有List<ConfNode>类型的成员变量Children用于保存其子节点;
- 前者没有能直接体现当前CItem对象是否为叶子节点的成员变量,后者有个布尔型的bIsLeaf成员变量用于判断是否为叶子节点,以便决定是否要拼接其值。
我们为了建立起树状结构,首先要把CItem中的内容逐个转移到ConfNode中,并将ConfNode中的父子关系、是否为叶节点等属性设置好,最终得到一个或者多个ConfNode根节点,这些根节点组成的List就是一个森林。
这个ConfNode的具体定义如下:
public class ConfNode {
private String name; // 节点名
private String nodeVal = ""; // 节点值
private Map<String, String> property = new LinkedHashMap<String, String>(); // 属性
private boolean bIsLeaf = true; // 是否叶子节点
private List<ConfNode> lsChildren = new ArrayList<ConfNode>(); // 子节点
public ConfNode(String name) {this.name = name;}
public String getName() {return name;}
public void setName(String name) {this.name = name;}
public String getNodeVal() {return nodeVal;}
public void setNodeVal(String nodeVal) {this.nodeVal = nodeVal;}
public Map<String, String> getProperty() {return property;}
public void setProperty(Map<String, String> property) {this.property = property;}
public boolean isLeaf() {return bIsLeaf;}
public void setIsLeaf(boolean isleaf) {this.bIsLeaf = isleaf;}
public List<ConfNode> getLsChildren() {return lsChildren;}
public void setLsChildren(List<ConfNode> lsChildren) {
this.lsChildren = lsChildren;
if (this.bIsLeaf && this.lsChildren.size() > 0) {
this.bIsLeaf = false;
}
}
/**
* 添加属性
* @param key
* @param value
*/
public void addProperty(String key, String value) {
this.property.put(key, value);
}
/**
* 添加子节点
* @param el
*/
public void addChild(ConfNode el) {
this.lsChildren.add(el);
if (this.bIsLeaf && this.lsChildren.size() > 0) {
this.bIsLeaf = false;
}
}
}
这个类主要有5个成员变量,接下来先逐一讲解。
- name:没啥好说的,就是节点的名字,不过需要注意的是,这个name和ConfNodeName有点不同,例如,Tab. 2中节点的ConfNodeName为Item|Src,那么对应的name为Src。
- nodeVal:节点的值,如果是叶节点,也就是说没有子节点,那么就会有非空的节点值。
- property:这是一个用于保存属性key-value对的映射表,也就是存放例如表Tab. 2中的ID、Title、CorpNo、ShowTopBar及其对应值的映射表。值得一提的是,这里最好用LinkedHashMap而不是HashMap,因为前者是有序的,前者是后者的一个子类,后者是无序的,如果你希望节点的属性是按照添加顺序输出的,那么最好用LinkedHashMap。关于LinkedHashMap和HashMap的详细用法,可以自行搜索网上资料。
- bIsLeaf:当前节点是否为叶节点标志,true or false。
- lsChildren:保存子节点的List。
实际上,在2.1.3中所提到的CItem类,除了要包含名字、值、索引三个变量以外,还应提供一系列的方法,以便转存到ConfNode中。其实现应当如下:
public class CItem implements Comparable<CItem>{
private String ConfNodeName; //配置节点名称
private String ConfNodeValue; // 配置节点项VALUE
private String ConfNodeIndex; // 排序
public String getConfNodeName(){return this.ConfNodeName;}
public void setConfNodeName(String val){this.ConfNodeName=val;}
public String getConfNodeValue(){return this.ConfNodeValue;}
public void setConfNodeValue(String val){this.ConfNodeValue=val;}
public String getConfNodeIndex(){return this.ConfNodeIndex;}
public void setConfNodeIndex(String val){this.ConfNodeIndex=val;}
//To read the first number of index
public int readRootIndex(String strIndex){
int rs = -1;
if(null == strIndex || strIndex.length() <= 0){
return rs;
}
int offset = strIndex.indexOf("|");
if (-1 == offset){
rs = Integer.parseInt(strIndex);
return rs;
}
String str = strIndex.substring(0,offset).trim();
rs = Integer.parseInt(str);
return rs;
}
//Compare the level of the dst to that of the src by index.
//1:higher
//-1:lower
// 0:equal
private int compareLevelByIndex(CItem src, CItem dst){
int rs = 0;
String strSrcIndex = src.getConfNodeIndex();
String strDstIndex = dst.getConfNodeIndex();
while(null != strSrcIndex && null != strDstIndex){
int iSrc = readRootIndex(strSrcIndex);
int iDst = readRootIndex(strDstIndex);
if (iDst > iSrc){
return -1;
}
else if (iDst < iSrc){
return 1;
}
else {
//读取最高层级
int offsetSrc = strSrcIndex.indexOf("|");
int offsetDst = strDstIndex.indexOf("|");
//如果源没了,但是目标还有,说明源高级
if(-1 == offsetSrc && -1 != offsetDst){
rs = -1;
return rs;
}
else if(-1 != offsetSrc && -1 == offsetDst){
rs = 1;
return rs;
}
//如果都没了,说明相等
else if(-1 == offsetSrc && -1 == offsetDst){
rs = 0;
return rs;
}
//否则继续截取“|”后边的子串,以比较下一级
strSrcIndex = strSrcIndex.substring(offsetSrc+1);
strDstIndex = strDstIndex.substring(offsetDst+1);
}
}
return rs;
}
//Compare the level of the dst to that of the src by name.
//1:higher
//-1:lower
// 0:equal
private int compareLevelByName(CItem src, CItem dst){
int rs = 0;
int numOfSrc = 0;
int numOfDst = 0;
String strSrc = src.getConfNodeName();
String strDst = dst.getConfNodeName();
while (null != strSrc){
int iSrc = strSrc.indexOf("|");
if(-1 == iSrc){
break;
}
strSrc = strSrc.substring(iSrc+1);
numOfSrc++;
}
while (null != strDst){
int iDst = strDst.indexOf("|");
if(-1 == iDst){
break;
}
strDst = strDst.substring(iDst+1);
numOfDst++;
}
//The more "|", the lower level.
return numOfDst == numOfSrc ? 0 : (numOfDst > numOfSrc ? -1 : 1);
}
//To get the last offset of pipe symbol |
public int getTailOffPipeSymbol(String str){
//It means input null if return -1 !!
int rsOffset = -1;
int count = 0;
while(null != str){
int len = str.length();
int Offset = str.indexOf("|");
if(-1 == Offset ){
break;
}
rsOffset += Offset;
count++;
if(Offset >= len-1){
break;
}
str = str.substring(Offset+1);
}
rsOffset = rsOffset + count;
return rsOffset;
}
//To get the short name of the node instead of the long one with the whole directory.
public String getShortConfNodeName(){
String strRs = new String(this.getConfNodeName());
int offset = getTailOffPipeSymbol(strRs) + 1;
strRs = strRs.substring(offset);
return strRs;
}
//is property (or node)
public boolean isProperty(){
boolean rs = false;
int numOfSeperatorIndex = 0;
int numOfSeperatorName = 0;
String strIndex = this.getConfNodeIndex();
String strName = this.getConfNodeName();
while (null != strIndex){
int iIndex = strIndex.indexOf("|");
if(-1 == iIndex){
break;
}
strIndex = strIndex.substring(iIndex+1);
numOfSeperatorIndex++;
}
while (null != strName){
int iName = strName.indexOf("|");
if(-1 == iName){
break;
}
strName = strName.substring(iName+1);
numOfSeperatorName++;
}
if(numOfSeperatorName > numOfSeperatorIndex){
rs = true;
}
return rs;
}
//Is current node the property of node src.
public boolean isProperty(CItem src){
boolean rs = false;
if(src.getConfNodeIndex() != this.getConfNodeIndex()){
return rs;
}
String strSrcName = src.getConfNodeName();
String strDstName = this.getConfNodeName();
int offset = strDstName.indexOf(strSrcName);
int srcLen = strDstName.length();
if(0 != offset || srcLen <= strSrcName.length()){
return rs;
}
String strDstShortName = strDstName.substring(srcLen);
if(strDstShortName.indexOf("|") == -1){
rs = true;
}
return rs;
}
//Is current node the child node of Src
public boolean isChild(CItem src){
boolean rs = false;
String strSrcIndex = src.getConfNodeIndex();
String strDstIndex = this.getConfNodeIndex();
if(this.isProperty() || src.isProperty()){
return false;
}
if(strDstIndex.indexOf(strSrcIndex) == -1 || strDstIndex == strSrcIndex){
return false;
}
int offset = strSrcIndex.length() + 1;
String strLastName = strDstIndex.substring(offset);
if(strLastName.indexOf("|") == -1){
rs = true;
}
return rs;
}
//Is current node a root node
public boolean isRoot(){
boolean rs = false;
if(this.isProperty()){
return rs;
}
String strIndex = this.getConfNodeIndex();
if(null == strIndex){
return rs;
}
int offset = strIndex.indexOf("|");
if(-1 == offset){
rs = true;
}
return rs;
}
//Is current node a leaf node
public boolean isLeaf(){
boolean rs = false;
if(this.isProperty()){
return rs;
}
String strVal = this.getConfNodeValue();
if(null != strVal && 0 < strVal.length()){
rs = true;
}
return rs;
}
//Compare the level of the dst to that of the src.
//1:higher
//-1:lower
// 0:equal
@Override
public int compareTo(CItem otherBp) {
int rs = compareLevelByIndex(this,otherBp);
if(0 == rs) {
rs = compareLevelByName(this, otherBp);
}
return rs;
}
}
这个保存配置项的类实现了Comparable接口,以便在拼装的时候,通过使用Collections.sort()方法来排序,使输入参数List<CItem>是有序的。如果无序,拼装会有难度,至少算法复杂度会是O(n*n),因为一个节点在寻找其父节点时,每次都会从无序的List中搜索。
既然我们希望List<CItem>有序,那么这个List到底是按照什么顺序呢?回到Tab. 2看一下,这个排列就是我们所需要的顺序。具体来讲,就是:
- 先从ConfNodeIndex来看,当前节点的层级越多(竖杠“|”数量越多),其排序优先级越低(越往后排),例如Item的ConfNodeIndex是0,Item|Src的是0|0,那么Item排序显然高于Item|Src;
- 在两个节点的ConfNodeIndex层级相同的情况下,如果ConfNodeIndex中从左往右数起,竖杠“|”间的数字编号,第一个出现较小的数字者优先级越高(越往前排),例如Item|Src是0|0,Item|CorpName是0|1,从左往右数,第一个数字都是0,第二个数字起,前者是0,后者是1,那么前者高于后者;
- 至于ConfNodeIndex完全相同者,如果一个是节点(ConfNodeName的竖杠“|”数量与ConfNodeIndex相同),一个是属性(ConfNodeName的竖杠“|”数量比ConfNodeIndex的多1条),那么节点高于属性;
- 如果ConfNodeIndex都相同,而且两者都是属性,那么属性之间的排列谁高谁低可以不用去管,反正这些属性只是若干组key-value的映射关系而已,添加到ConfNode对象节点中使用的时候是用LinkedHashMap按照添加顺序输出的;
- 如果ConfNodeIndex都相同,而且两者都是节点……那是不可能的事情,不存在的,这样的话拼装时就乱了套了,就会撞到一起。
我们按照上述这5项比较规则(实际上只有前3项哈)在这个CItem类中实现了compareLevelByIndex和compareLevelByName这两个方法,并在重写compareTo这个方法时,调用了这两个方法,输入为待比较的CItem对象,输出为大小比较标志,1为大于,0为等于,-1为小于。如果不清楚,具体的Comparable接口以及Collections.sort()排序方法可以自行搜索网上资源进行查询。
在建立起如图Fig. 2 所示的树状结构之后,我们可以便可以从根节点开始,递归地遍历配置节点,并拼接配置文件了。
ini格式的文件是只有节点没有属性的,而xml格式文件既有节点又可能有属性,这一点通过观察Tab. 1和Tab. 2可以看出来。
总结一下,这个类的主要作用有:
- 保存配置项数据;
- 让配置项数据能够有序地保存到ConfNode节点中,并构建起ConfNode节点树或者森林(ConfNode根节点的List)。
这个类的成员函数比较多,但是主要功能都是为了辅助实现上述提到的两个作用,而且代码中包含有注释,不难理解其含义,这里不再赘述。(英文注释是为了防止乱码,中文注释是英文编不下去了才写的…各位看官有实在看不明白之处可以联系我…)
2.3.1.2.建立配置节点森林
这一步实际上就是每一棵配置节点树建立好了添加到List里边就好,这个List就可以看作是一个配置节点森林。在2.3.1.1中建立起一个有序的List<CItem>后,我们利用这个List作为输入,开始对这个森林进行创建了(输出为List<ConfNode>)。
对于森林的创建,大体上的思路就是在这个有序的List<CItem>中自上而下地进行遍历。由于xml格式比ini格式更为复杂,更为通用,我们以xml格式建立森林为例,我们可以对照着Tab. 2进行从上到下搜索:
- 第一个项是Item,索引为0,ConfNodeValue为空,设置为非叶节点,那么创建对应的ConfNode,并连同其节点名(不包含竖杠“|”,包含竖杠的要去掉,取ConfNodeName最右的名字)添加到一个名为mapStrCNode的Map中,以便后面找到有该节点属性的时候,将属性保存到该节点的成员变量property中;然后由于本节点的ConfNodeIndex和ConfNodeName中都没有竖杠,因此该节点是根节点,添加到一个用于输出结果的List<ConfNode>类型的lsCNode中,后续如果再有根节点则继续添加;
- 到第二个项时,Item|ID的索引为0,因此该项是属性,然后在表中向上寻找其归属的节点Item,找到后添加进去;
- 到第六个项时,Item|Src的索引为0|0,因此该项是节点,而且ConfNodeValue非空,设置为叶子节点,然后从前一个项的位置向上搜索其父节点,搜索到Item了,OK,将本节点添加到Item的子节点列表中,Item节点设置为非叶节点;
- 后面的项依次类推。
总结了一下,大致就是,对有序的List<CItem>表进行从高到低的遍历,遍历到的节点就创建,然后从前面创建过的节点中寻找到父节点并添加(有序的List,其父节点必然在其前面,而且一般是顺着前一项向上找最快,其中原因请结合排序规则进行思考),如果遍历到了属性就往前寻找所属节点,并加入到节点的属性Map中。值得注意的是,ini格式的文件中没有属性项存在,但是构建森林的代码仍然通用。
配置节点建立的过程如图Fig. 6所示:

配置节点森林的建立,是单独定义了一个类,然后在类里面实现一个创建节点森林的方法。其代码实现如下:
public class TreeUtil {
public static List<ConfNode> BuildConfigNodeForest(List<CItem> lsBp){
List<ConfNode> lsRs = new ArrayList<ConfNode>();
Map<String, ConfNode> mapStrCNode = new HashMap<String, ConfNode>();
int lsBpSize = lsBp.size();
for(int i = 0; i < lsBpSize; i++){
CItem clsBpHead = lsBp.get(i);
//Is it a Property
if(clsBpHead.isProperty()){
for(int j = i-1; j>=0; j--){
CItem clsBpTemp = lsBp.get(j);
//First found non-property node, add the current prop to it.
if(clsBpHead.isProperty(clsBpTemp)){
ConfNode confNode;
String strName = clsBpTemp.getShortConfNodeName();
confNode = mapStrCNode.get(strName);
String strKey = clsBpHead.getShortConfNodeName();
String strVal = clsBpHead.getConfNodeValue();
confNode.addProperty(strKey, strVal);
//Adding prop finished
break;
}
}
}else{
//else it is a node,
ConfNode clsCNode;
String strName = clsBpHead.getShortConfNodeName();
clsCNode = new ConfNode(strName);
mapStrCNode.put(strName, clsCNode);
//Is it a Leaf Node
if(clsBpHead.isLeaf()){
clsCNode.setIsLeaf(true);
clsCNode.setNodeVal(clsBpHead.getConfNodeValue());
}else{
clsCNode.setIsLeaf(false);
}
//Is it a Root Node
if(clsBpHead.isRoot()){
lsRs.add(clsCNode);
continue;
}
// if it is a child node, we have to find its parent
for(int j = i-1; j>=0; j--){
CItem clsBpParent = lsBp.get(j);
//First found its parent node, add the current child to it.
if(clsBpHead.isChild(clsBpParent)){
//Parent node found
ConfNode parentNode;
String strParentName = clsBpParent.getShortConfNodeName();
parentNode = mapStrCNode.get(strParentName);
parentNode.setIsLeaf(false);
//Is it a Root Node
if(clsBpParent.isRoot() && !lsRs.contains(parentNode)){
lsRs.add(parentNode);
}
ConfNode childNode = clsCNode;
parentNode.addChild(childNode);
break;
}
}
//End for
}
}
return lsRs;
}
}
我在这代码里面已经加上了应有的注释,结合流程图Fig. 6以及前面所提到的规则,只要有耐心去慢慢阅读,应该不难理解其意思。
2.3.2.ini配置文件的拼装
对于节点森林的构建,我们已经在前面的步骤中完成了,接下来就是从构建好的森林去拼接配置文件了。对于ini文件,其结构真的不能再简单了,简单得我都不想去费这个篇幅去讲了,但是想想,还是写上吧,或许便于对xml文件拼装的理解。
如图Fig. 7所示,ini拼装过程相对简单,构建好节点森林后(对应步骤①),每一棵配置节点树的层数都是2,也就是说,每一棵树的拼装内容只有头[Section]和其key-value对(对应步骤②)。过程比较简单,不多赘述。

单棵ini配置节点树拼接流程对应的代码实现如下:
public class IniAssembleUtil {
public static String lt = "[";
public static String rt = "]";
public static String quotes = "\"";
public static String equal = "=";
public static String blank = " ";
public static String nextLine = "\r\n";// 换行
/**
* @category 拼接INI节点信息
* @param confNode
* @return
*/
public static StringBuffer confNodeToIni(ConfNode confNode) {
StringBuffer result = new StringBuffer();
// 元素开始
result.append(lt).append(confNode.getName()).append(rt);
result.append(nextLine);
for (ConfNode temp : confNode.getLsChildren()) {
result.append(temp.getName());
result.append(equal);
result.append(temp.getNodeVal());
result.append(nextLine);
}
return result;
}
}
所有ini配置节点树的拼接,实际上就是每一个节点树的拼接结果依次组合,其代码实现如下:
public class IniConverter {
public static String assembleAsIni (List<CItem> lsItem){
String strIni = null;
//Generate a ConfNode root list
List<ConfNode> lsConfNodeRoot;
StringBuffer sbIni = new StringBuffer();
lsConfNodeRoot = TreeUtil.BuildConfigNodeForest(lsItem);
for (ConfNode rootNode : lsConfNodeRoot) {
sbIni.append(IniAssembleUtil.confNodeToIni(rootNode));
}
try{
strIni = new String(sbIni.toString().getBytes(), "UTF-8");
}catch(Exception e){
e.printStackTrace();
}
return strIni;
}
}
下面可以通过对比xml拼接流程来加深整个拼装思路的理解。
2.3.3.xml配置文件的拼装
xml文件的拼装流程如Fig. 8所示,该流程是通过遍历根节点数组,对每个根节点依次进行递归遍历,最终得到有序的xml字符串。
Fig. 7中的步骤①和Fig. 8中的步骤①完全一样,不同之处在于步骤②,前者的配置节点树只有2层,而后者可能会有2层以上,而且后者的每一层节点都可能含有属性。

xml配置文件拼接流程的代码实现如下:
public class XmlConverter {
public XmlConverter(){}
public static String assembleAsXml (List<CItem> lsBp) throws Exception{
if(null == lsBp || lsBp.size() == 0){
return null;
}
//对对象列表进行排序
Collections.sort(lsBp);
//开始进行节点树的构建
//Generate a ConfNode root list
List<ConfNode> lsConfNodeRoot;
String strXml = null;
StringBuffer sbXml = new StringBuffer();
lsConfNodeRoot = TreeUtil.BuildConfigNodeForest(lsBp);
for (ConfNode rootNode : lsConfNodeRoot) {
//sbXml.append(XmlAssembleUtil.confNodeToXml(rootNode)).append("\r\n"); //无缩进版
sbXml.append(XmlAssembleUtil.confNode2IndentXml(rootNode,0)).append("\r\n"); //带缩进版
}
try{
strXml = new String(sbXml.toString().getBytes(), "UTF-8");
}catch(Exception e){
e.printStackTrace();
}
return strXml;
}
}
代码中的assembleAsXml方法就是xml文件拼装的实现。
在Fig. 8中,步骤①建立配置节点森林的过程已经在2.3.1.2介绍了,接下来主要介绍其中的步骤②,递归地遍历每一棵配置节点树。该遍历过程实际上就是按顺序遍历并拼接字符串,详细过程如图Fig. 9所示。

对于Fig. 9的拼装流程,总体上可以分为3个部分,即:
①<nodeName key=“value”>
② ……
③</nodeName>
其中最关键的部分就是第②部分,这一步如果有子节点,则通过递归调用的方式,来实现多个层级子节点内容的拼装;否则直接拼接当前节点的字符串内容。这个流程图就是confNodeToXml方法的实现过程,请结合2.3.1.1中所定义的ConfNode类节点数据结构,来理解confNodeToXml方法的代码:
public class XmlAssembleUtil {
public static String lt = "<";
public static String ltEnd = "</";
public static String rt = ">";
public static String rtEnd = "/>";
public static String quotes = "\"";
public static String equal = "=";
public static String blank = " ";
/**
* @category 拼接XML各节点信息
* @param confNode
* @return
*/
public static StringBuffer confNodeToXml(ConfNode confNode) {
StringBuffer result = new StringBuffer();
// 元素开始
result.append(lt).append(confNode.getName());
// 判断是否有属性
if (confNode.getProperty() != null && confNode.getProperty().size() > 0) {
Iterator<String> iter = confNode.getProperty().keySet().iterator();
while (iter.hasNext()) {
String key = String.valueOf(iter.next());
String value = confNode.getProperty().get(key);
result.append(blank).append(key).append(equal).append(quotes)
.append(value).append(quotes);
}
}
result.append(rt);// 结束标记
/*
* 判断是否是叶子节点 是叶子节点,添加节点内容 不是叶子节点,循环添加子节点
*/
if (confNode.isLeaf()) {
result.append(confNode.getNodeVal());
} else {
for (ConfNode temp : confNode.getLsChildren()) {
result.append(confNodeToXml(temp));
}
}
// 节点结束
result.append(ltEnd).append(confNode.getName()).append(rt);
return result;
}
/**
* @category 拼接XML申明信息
* @param confNode
* @return
*/
public static String confNode2Xml(ConfNode confNode) {
StringBuffer body = confNodeToXml(confNode);
StringBuffer head = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n");
head.append(body);
return head.toString();
}
}
至此,xml文件的拼装流程告一段落。
3.后记
3.1.总结
ini、xml配置文件的解析和拼装,实际上是对这些配置文件树结构的拆分和构建,充分利用递归的方式可以节省代码量。在对每一项记录进行遍历之前,对List进行排序可以减少后续的时间复杂度,磨刀不误砍柴工。
3.2.代码下载及使用
3.2.1.代码下载链接
拼装和解析的代码下载链接:
https://gitee.com/vincent_yu/ConfigFile/
2.3.2代码目录结构以及使用方式
整个代码的目录结构如图所示。

util包下是对应配置文件解析和拼装的实现代码。
treeutil包是负责将CItem类型记录项的列表转化成有序的ConfNode节点森林的代码。
iniutil负责ini配置文件的解析和拼装,ParseIniUtil负责解析ini文件成CItem对象列表,IniAssembleUtil负责单棵配置节点树的拼装,IniConvert调用单棵配置节点树拼装的方法实现所有树的拼接。
xmlutil负责xml配置文件的解析和拼装,ParseXmlUtil负责解析xml文件成CItem对象列表,XmlAssembleUtil负责单棵配置节点树的拼装,XmlConvert调用单棵配置节点树拼装的方法实现所有树的拼接。
com.vin下的TestDemo是测试配置文件解析和拼装的主程序入口类。里面的注释将ini的解析、拼装和xml的解析、拼装测试代码分成4个部分,测试哪一部分就留下哪一部分,其他部分注释掉。
欢迎关注!