还在使用XML仅作为简单读写的配置文件吗?其实XML还有更好更实用的操作…
概述 在Java中使用XML可谓是极为简单,JDOM问世把Java与XML的关系更近了。但是JDOM提供对于XML简单的静态操作。如果把XML作为动态配置文件或者模块安装配置文件(至少在我开发的系统中作为),仅仅使用JDOM的方法就显得有点儿力不从心了,因此,我们要充分利用JDOM的优点开发出对XML文件的高级操作库。
我们要开发什么样的操作库呢?首先,看下面的XML信息:
<?xml version="1.0" encoding="UTF-8"?> <config> <dirs> <home>/home/myuser</home> <tmp>/home/myuser/tmp</tmp> <var>/home/myuser/var</var> </dirs> </config>
这是一个简单的配置文件,其中重复使用主目录路径信息,很麻烦不是吗?不过这里有很好的解决方案就是使用XML内部实体,如下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE config [ <!ENTITY home "/home/myuser "> ]> <config> <dirs> <home>&home; </home> <tmp>&home; /tmp</tmp> <var>&home; /var</var> </dirs> </config>
这是个不错的办法,通过定义实体可是替换那些重复使用的信息。不过我还是要实现自己的方法,至于它们的区别,我会在下面讲述。 变量 因为我是程序员,看到这样的问题首先会想到"变量"这个词。为什么不使用一个特殊的元素作为变量的定义呢?OK,开始让我们实现吧。请看下面的示例:
<?xml version="1.0" encoding="UTF-8"?> <config> <property name="home" value="/home/myuser"/> <dirs> <!-这段可以省略了' <home>${home} </home> <property name="home" value="${home}/tmp"/> <tmp path="${dirs.home}"> <tmp1>${dirs.home}/tmp1</tmp1> </tmp> <var>${home}/var</var> </dirs> </config>
在这里我使用了property元素作为变量的定义元素。咦?看懂了没有?有两个home变量啊,那该怎么解决冲突呢?正如上面显示的,使用 元素名称+"."+变量名称 作为该变量的定义以解决冲突如何?那就这么做吧。
OK,配置变量规则都设计出来了该写解释器了。
因为可能以后还会增加额外的解释器,我们先定义一个解释器接口DocumentParser:
import org.jdom.Document; public interface DocumentParser{ public void parse(Document doc); }
既然接口已定义好,就该写变量解释器了。
/ ** * property元素解释器。 */ public class DocumentPropertyParser implements DocumentParser{ private Properties variables;
public DocumentPropertyParser(){ variables = new Properties(); }
public void parse(Document doc){ Element element = doc.getRootElement(); String path = element.getName(); //解析变量并添加到variables属性对象中 parseVariables(path, element); //分析元素属性和值, 替换已解析的变量. parseContent(element); }
/ ** * 解析变量方法 */ private void parseVariables(String path, Element element){ List subList = element.getChildren(); Iterator elementI = subList.iterator(); while(elementI.hasNext()){ Element elementS = (Element)elementI.next(); String name = elementS.getName();
//处理变量, 这里忽略了property元素名称的大小写. //否则, 递归处理里面的元素. if("property".equalsIgnoreCase(name)){
//获取变量名称. String propName = elementS.getAttributeValue("name");
//如果变量名称不为空, 则先获取名称为value的属性. if(propName != null){ String propValue = elementS.getAttributeValue("value");
//如果名称为value的属性不存在, 则获取该元素的值. if(propValue == null) propValue = elementS.getText();
//如果其值也不存在, 则该变量的值取空. if(propValue == null) propValue = "";
//向变量列表中加入该变量信息. variables.setProperty(path + "." + propName, propValue); } //删除属性字段, 避免冲突. elementI.remove(); } else{ parseVariables(path + "." + name, elementS); } } }
/ ** * 处理元素中的变量. */ private void parseContent(Element element){ //首先, 检查该元素属性值是否存在变量. List attributes = element.getAttributes(); Iterator attributeI = attributes.iterator(); while(attributeI.hasNext()){ Attribute attribute = (Attribute)attributeI.next(); String attributeValue = attribute.getValue();
//使用字符串替换方法替换变量. Enumeration propNames = variables.propertyNames(); while(propNames.hasMoreElements()){ String propName = (String)propNames.nextElement(); String propValue = variables.getProperty(propName); attributeValue = StrUtil.replace("${" + propName + "}", propValue, attributeValue); } //重新设置替换过的变量值. attribute.setValue(attributeValue); } //如果有子信息, 递归检索子信息, 否则检索其值. List subElements = element.getChildren(); if(subElements.size() != 0){ //递归检索Element. Iterator subElementI = subElements.iterator(); while(subElementI.hasNext()){ Element subElement = (Element)subElementI.next(); parseContent(subElement); } } else{ String value = element.getText(); if(value != null){ //覆盖变量. Enumeration propNames = variables.propertyNames(); while(propNames.hasMoreElements()){ String propName = (String)propNames.nextElement(); String propValue = variables.getProperty(propName); value = StrUtil.replace("${" + propName + "}", propValue, value); } element.setText(value); } } } }
注:StrUtil.replace方法是一般字符串替换方法,自行实现。
OK,简单的变量解释器完工了。 再想想还需要什么… 系统变量 有时候,配置文件需要一些系统的信息作为参考(我称为系统变量),而系统信息是在运行期才可以得到的,不过我们可以通过预定义一些变量名称使在配置文件中可以提前使用,通过解释系统变量来动态为该元素赋值。例如这个配置文件:
<?xml version="1.0" encoding="UTF-8"?> <config> <property name="version" value="1705"/> <module version="%{system.version}.${version}" home="%{system.home}/helloword"> … … </module> </config>
解析后变成了:
<?xml version="1.0" encoding="UTF-8"?> <config> <module version="1.0.1705" home="/home/myuser/helloworld"> … … </module> </config>
这里使用了我们预定义的系统变量"system.version"和"system.home"分别表示系统版本和系统的主目录。这使得配置文件具备获取系统动态信息的能力。下面我们用Java实现系统变量解释器DocumentConstantParser:
public final class DocumentConstantParser implements DocumentParser{ private Properties constants;
public DocumentConstantParser(){ constants = new Properties(); }
public DocumentConstantParser(Properties original){ constants = new Properties(original); }
//通过运行期设置预定义的变量, 配置文件可以动态读取这些信息. public void addConstant(String name, String value){ constants.setProperty(name, value); }
public void removeConstant(String name){ constants.remove(name); }
public void parse(Document doc){ parseContent(doc.getRootElement()); } … }
因为parseContent方法跟变量解释器的几乎相同,所以节省点儿空间就不写出了。系统可以通过使用addConstant和removeConstant方法操作对XML文档预定义的变量,这使得XML的配置功能更强大了。
比较前述的使用实体,此方法易于跟系统交互,且方法简便易懂。 命令 曾经研究过Ant,其中的<javac>等元素十分吸引我。能不能再添加一个具有命令解释能力的元素呢?答案是肯定的。 我定义XML文档中的command元素作为命令的入口元素。通过实现具体命令实现执行该命令。首先,因为可能有很多命令,我们定义一个命令接口DocumentCommand:
public interface DocumentCommand{
//检查是否接受指定名称的命令。 public boolean accept(String name);
//处理命令 public void invoke(Document doc, Element current, DocumentCommandParser parser) throws JDOMException; }
定义好了命令解释接口,就可以设计解释器了。Java代码如下:
public final class DocumentCommandParser implements DocumentParser{ private HashSet commands; private boolean parsing; public DocumentCommandParser(){ commands = new HashSet(10); parsing = false; }
//添加命令解释单元 public synchronized void addCommand(DocumentCommand dc){ if(parsing) try{ wait(); }catch(InterruptedException ie){}
if(commands.contains(dc)) commands.remove(dc); commands.add(dc); }
//删除命令解释单元 public synchronized void removeCommand(DocumentCommand dc){ if(parsing) try{ wait(); }catch(InterruptedException ie){} commands.remove(dc); }
public synchronized void parse(Document doc) throws JDOMException{ parsing = true; parseCommand(doc, doc.getRootElement()); parsing = false; notifyAll(); }
private void parseCommand(Document doc, Element element) throws JDOMException{ List sub = element.getChildren(); for(int i = 0;i < sub.size();i++){ Element subElement = (Element)sub.get(i);
//检查该元素是否是命令元素, 如果不是递归解释该元素的子元素. if(!"command".equalsIgnoreCase(subElement.getName())){ parseCommand(doc, subElement); } else{
//删除命令元素, 避免冲突. sub.remove(i--);
//获取命令名称,如果为空, 则继续. String commandName = subElement.getAttributeValue("name"); if(commandName != null){
//开始检索适合的命令处理器. Iterator cmdI = commands.iterator(); processing: while(cmdI.hasNext()){ DocumentCommand dc = (DocumentCommand)cmdI.next();
//找到对应命令处理器后, 进行命令解释. if(dc.accept(commandName)){
//调用命令处理方法, 如果执行成功, 删除该变量. dc.invoke(doc, subElement, this);
//中断命令检索过程. break processing; } } } } } } }
终于实现了命令解释功能,下面来研究一下它的用途。 例如,一个文件拷贝命令filecopy有两个属性,source为源文件路径,target为拷贝目的路径,通过这一命令可以实现众多功能,如组件安装时从其他空间拷贝数据到其主目录。代码如下:
… … <command name="filecopy" source="${home} /lib" target="%{system}/lib/module"/> … …
这样,组件所使用到的库文件就拷贝到了系统的库中。下面是filecopy的代码:
public class FileCopyCommand implements com.yipsilon.util.jdom.DocumentCommand{ public boolean accept(String s){ //只有filecopy命令可以接受. return "filecopy".equals(s); }
public void invoke(Document document, Element element, DocumentCommandParser parser){ String name = element.getAttributeValue("name"); //值为: filecopy String source = element.getAttributeValue ("source"); //值为原路径 String target = element.getAttributeValue ("target"); //值为目的路径 FileUtil.copy(source, target); //因内容有限, 这里没有增加额外的错误检查. } }
注:FileUtil.copy是简单的文件拷贝方法, 请自行实现。 结论 通过在XML文档中使用变量、系统变量、命令大大增强了XML作为配置信息集合的功能。通过实现不同的解释器和命令单元,可以使其功能不断增多从而实现一些以前无法实现的目的,比如作为安装文件进行环境检测、I/O操作等,既能提高开发速度,内容也好管理。
Java与XML与生俱来,在它们的身上可以开发出很多简化开发的产品,如Ant。有时候只需要动动脑筋就可以使开发变得简单。
| |