dom4j本身提供了两种解析xml的方式:dom解析和sax解析。关于dom解析和sax解析各自的优缺点这里不再多述,只强调的一点是由于越来越多的应用会遇到大数据场景,SAX解析方式刚好是解决此类场景的完美方案,因此“DOM4J解析大数据的方案”就是"如何利用SAX方式解析大数据的方案"(当然JAXP中的sax解析也是同样的方案),本文梳理总结下实际工作中使用DOM4J解析大容量XML文件的实现。
XML文件结构以滴答团购数据为例:
<urlset>
<url>
<loc>http://beijing.didatuan.com/team.php?id=130515</loc>
<data>
<display>
<website>嘀嗒团</website>
<siteurl>http://beijing.didatuan.com</siteurl>
<city>北京</city>
<title>【王府井】仅26.5元,享市场价100元『北京横店影视电影城』单人电影票一张!2D、3D通兑!</title>
<category>2</category>
<subcategory>电影</subcategory>
<dpshopid/>
<range>王府井/东单</range>
<major>1</major>
<address>北京市东城区王府井大街253号王府井百货北馆8楼</address>
<image>
http://i2.didaimg.com/pic/team/2013/0809/mobilew/13760173024175.jpg
</image>
<startTime>1376236800</startTime>
<name>横店影视电影城单人票</name>
<seller>北京横店影视电影城</seller>
<phone/>
<endTime>1388419200</endTime>
<value>100.00</value>
<price>26.50</price>
<rebate>2.65</rebate>
<bought>46023</bought>
</display>
<shops>
<shop>
<seller>北京横店影视电影城</seller>
<phone>010-65231588</phone>
<addr>北京市东城区王府井大街253号王府井百货北馆8楼</addr>
<longitude>116.41093969345093</longitude>
<latitude>39.91338185899138</latitude>
<trafficInfo>乘坐1号线王府井站C口出,沿王府井步行街步行约5分钟</trafficInfo>
<range>王府井/东单</range>
</shop>
</shops>
</data>
</url>
<!--..大量url节点..-->
</urlset>
dom4j读取、解析xml文件的方法:
SAXReader reader = new SAXReader();
Document document = reader.read(new File("..."));
如果只是这么简单的去读取解析大文件有很大的可能会遇到内存溢出的异常,不要说1G的文件了,就算是100M都有可能会异常。这种方式还会引出另一个疑问:SAXReader这样read文件究竟是用的SAX解析方式还是DOM解析方式?如果没有去查阅其API文档或者分析源码的话,仅从其使用方式看好像是dom解析:毕竟读出来一个完整的Document对象然后在其他地方再继续处理这个文档对象。事实果真如此么?要是不嫌麻烦就去看看源码吧,嫌麻烦就看看API吧。
事实上,就像其类名已经明确指出的一样,SAXReader的内部解析方式就是sax方式。并且提供了很灵活的句柄接口以便在sax解析过程中不同时刻触发不同事件时调用句柄接口,这也是与jdom相比,dom4j更多的被称赞为面向接口方案的优秀特征。重点分析下dom4j定义的事件句柄接口ElementHandler:
org.dom4j
Interface ElementHandler
public interface ElementHandler
ElementHandler interface defines a handler of Element objects. It is used primarily in event based processing models such as for processing large XML documents as they are being parsed rather than waiting until the whole document is parsed.
Version:
$Revision: 1.8 $
Author:
James Strachan
Method Summary
void onEnd(ElementPath elementPath)
Called by an event based processor when an elements closing tag is encountered.
void onStart(ElementPath elementPath)
Called by an event based processor when an elements openning tag is encountered.
与jaxp相关事件处理器的定义相比,即使是方法的名字也更显得"纯正化"。ElementHandler事件处理器在sax解析过程中至关重要,熟悉其原理和规则就能有技巧的实现实际需求。它仅仅定义了两个方法:onEnd和onStart,其被调用时机文档说得很清楚了,在时机被触发时分别干什么事就是该接口的具体实现要考虑的。
需要特别说明的是,SAX解析之所以能利用很小的、有限的内存空间去解析大容量的数据,秘密也在于这两个方法,即该处理器被绑定的文档节点在被流式读取完毕时需要释放掉其已占用的内存空间,无论具体的业务逻辑实现是怎样的,这一步是必须要实现的公共逻辑,否则SAXReader解析xml时会将该节点纳入其最终返回的文档模型对象document中,占用内存会与DOM方式解析一样越来越大。XML节点释放资源的方法是
Node detach()
Removes this node from its parent if there is one.
该方法由dom4j的dom模型树中的最顶级接口Node定义,因此所有节点都可以调用。
这里需要给出一个样例:
public class MainDataElementHandler implements ElementHandler {
private ElementParser<Bean> parser;
private Writer writer;
public MainDataElementHandler(ElementParser<Bean> parser, BufferedWriter writer) {
super();
this.parser = parser;
this.writer = writer;
}
@Override
public void onEnd(ElementPath path) {
Element urlNode = path.getCurrent();
Bean outputBean = null;
try {
outputBean = parser.parseElement(urlNode);
} catch (Exception e) {
e.printStackTrace();
} finally {
urlNode.detach();
}
if (outputBean != null) {
writer.write(outputBean);
}
}
@Override
public void onStart(ElementPath path) {
Element urlNode = path.getCurrent();
urlNode.detach();
}
}
个人认为这个样例虽然极为简单,但是已经解决了应该解决的大部分问题:
一 解决节点释放资源的问题
参考网上其他有价值的文章,有观点认为仅仅在onEnd方法中调用节点的detach()方法并不能真正释放资源,仍然会导致内存爆掉的现象,必须在onStart方法中调用该节点的detach()方法才有效,本人没有试过,为保险起见,在两个方法里都调用一次。这里可能会导致的一个疑问是如果在节点的start读取时刻就detach该节点,那么在onEnd时该节点还存在么?实际动手实验下,就会发现onEnd方法中Element urlNode = path.getCurrent();得到的urlNode节点对象是健全完整的,可以对其进行增删改查操作。这里的另一个疑问是如果节点真的只有在onStart方法中detach才能有效释放资源,那么为什么只在onEnd方法中detach就无效呢?毕竟按照正常逻辑思维在end后才释放资源是更自然的,由于没有跟踪查看源代码(鄙人也很懒),这里只有一个猜测:SAXReader在流式解析数据时,遇到某路径上绑定的处理器并触发之后,在onStart方法调用之后、onEnd方法调用之前,会改变最终返回的文档模型对象document的构造机制,这个操作肯定不会是当前节点调用detach方法时完成的(否则onEnd中调用也应有效),节点调用detach方法更大的可能实现是给该节点一个标识,标识不需要纳入最终的文档模型对象document。
二 解决对当前节点的解析问题
如果把"对当前节点的解析"放在onEnd方法本身中当然也是可行的,但是更好的设计思想是由专有的模块去实现,因为xml中不同的节点具有不同数据结构,为了更灵活的多样化的应对具体节点的解析,当然要"面向接口"编程了,这里定义了一个接口来做这个事情:ElementParser<T>
/**
* 专一职责:解析xml某种类型的节点,返回泛型化的数据类型T
* @author warhin
*
* @param <T>
*/
public interface ElementParser<T> {
T parseElement(Element e);
}
这个接口与ElementHandler接口的关系有些类似静态代理模式,姑且称之为"泛静态代理模式"吧。
事实上这个接口的定义不应该由用户来完成,应该由dom4j本身实现,只是不知道dom4j最新的版本有没有纳入类似的接口。三 解决业务逻辑的问题
最后,writer.write(outputBean);这行代码可以抽象的认为是业务逻辑的具体实现,不管是要将解析出来的pojo写入文件,还是存入数据库,还是输出到网络中等等。
目前看起来好像所有的问题都解决了,事实上,稍加思考,会发现流程上还有一个很重要的问题没有提及:ElementHandler如何被SAXReader绑定到具体的节点上?
dom4j给出了两种方案:
void addHandler(String path, ElementHandler handler)
Adds the ElementHandler to be called when the specified path is encounted.
void setDefaultHandler(ElementHandler handler)
When multiple ElementHandler instances have been registered, this will set a default ElementHandler to be called for any path which does NOT have a handler registered.
具体绑定方法动手实践下就知道了,这里也有几个很重要的问题需要思考:如果多个ElementHandler绑定同一个节点时,在解析时会全部触发么?或者一个节点能绑定多个ElementHandler么?经过测试,发现结论如下:dom4j对事件触发器是全局唯一的,这点儿有别于ECMA对于dom事件模型的规范定义,也就是说dom4j中不会有事件冒泡传播的特性。当然在SAXReader解析数据过程中也可以不设置事件触发器,就像文章最开始的解析那样,这下想必大家应该能猜测到那样调用dom4j的默认行为了吧。对于xpath上未绑定ElementHandler的节点可以用第二个方法设置全局默认触发器做一些公共事情,比如释放资源。
花了几个小时归纳整理这篇文章,只是因为在网上没有找到几篇有价值的文章,不管是用百度还是谷歌,能找到的也只是简单的对节点的增删改查操作。这里不保证本文全部观点的正确性,转载请注明。