我正在尝试使用javax.xml.xpath包在具有多个命名空间的文档上运行XPath表达式,而且我遇到了愚蠢的性能问题。
我的测试文档来自一个真实的生产示例。它大约是600k的xml。该文档是一个相当复杂的Atom提要。
我意识到我正在使用XPath做的事情可以在没有的情况下完成。然而,在其他非常低劣的平台上实现相同的实现表现得非常好。现在,重建我的系统以不使用XPath超出了我所能做的范围。
我的测试代码是这样的:
void testXPathPerformance()
{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(loadTestDocument());
XPathFactory xpf = XPathFactory.newInstance();
XPath xp = xpf.newXPath();
NamespaceContext names = loadTestNamespaces();
//there are 12 namespaces in names. In this example code, I'm using
//'samplens' instead of the actual namespaces that my application uses
//for simplicity. In my real code, the queries are different text, but
//precisely the same complexity.
xp.setNamespaceContext(names);
NodeList nodes = (NodeList) xp.evaluate("/atom:feed/atom:entry",
doc.getDocumentElement(), XPathConstants.NODESET);
for(int i=0;i<nodes.getLength();i++)
{
printTimestamp(1);
xp.evaluate("atom:id/text()", nodes.item(i));
printTimestamp(2);
xp.evaluate("samplens:fieldA/text()", nodes.item(i));
printTimestamp(3);
xp.evaluate("atom:author/atom:uri/text()", nodes.item(i));
printTimestamp(4);
xp.evaluate("samplens:fieldA/samplens:fieldB/&at;attrC", nodes.item(i));
printTimestamp(5);
//etc. My real example has 10 of these xp.evaluate lines
}
}
当我在Nexus One上运行时(不在调试器中,但连接了USB),第一次通过循环时,每个xp.evaluate需要10ms到20ms。到第15次循环时,每个xp.evaluate需要200ms到300ms。在循环结束时(nodes
中有150个项目),每个xp.evaluate需要大约500ms-600ms。
我尝试过使用xp.compile()。编译所有需要&lt; 5ms。我已经完成了xp.reset()(没有任何区别)。我为每个评估做了一个新的XPath对象(增加大约4毫秒)。
执行期间内存使用似乎不会失控。
我在JUnit测试用例中的单个线程上运行它,它不会创建任何活动或任何内容。
我真的很困惑。
有人知道还有什么可以尝试吗?
谢谢!
更新
如果我向后运行for循环(for(int i=nodes.getLength()-1;i>=0;i--)
),那么前几个节点需要500ms-600ms,最后几个节点快速运行10ms-20ms。因此,这似乎与调用的数量无关,而是上下文接近文档末尾的表达式比上下文接近文档开头的表达式花费的时间更长。
有人对我能做些什么有任何想法吗?
答案 0 :(得分:51)
尝试在顶部的循环中添加此代码;
Node singleNode = nodes.item(i);
singleNode.getParentNode().removeChild(singleNode);
然后使用singleNode
变量而不是nodes.item(i);
运行每个评估
(当然你改了名字)
执行此操作会从大型主文档中分离您正在使用的节点。这将大大加快评估方法的处理时间。
EX:
for(int i=0;i<nodes.getLength();i++)
{
Node singleNode = nodes.item(i);
singleNode.getParentNode().removeChild(singleNode);
printTimestamp(1);
xp.evaluate("atom:id/text()", singleNode );
printTimestamp(2);
xp.evaluate("samplens:fieldA/text()", singleNode );
printTimestamp(3);
xp.evaluate("atom:author/atom:uri/text()", singleNode );
printTimestamp(4);
xp.evaluate("samplens:fieldA/samplens:fieldB/&at;attrC", singleNode );
printTimestamp(5);
//etc. My real example has 10 of these xp.evaluate lines
}
答案 1 :(得分:12)
这似乎是另一种情况,使用XPath看起来很慢但不是XPath,原因可能是由DOM方法引起的nodelist.item(i)
Java中NodeList
的默认实现具有以下特定功能:
当你单独看这些特性时,你可能想知道为什么XPath表达式的结果对象具有这样的特征,但是当你把它们放在一起时它们会更有意义。
<强> 1)强> 延迟评估可能会模糊性能瓶颈的位置。因此,返回NodeList似乎很快,但如果任务要总是遍历列表,那么它或多或少只会延迟性能成本。如果每次读取列表中的下一个项目时必须再次处理整个列表的评估,则延迟评估会变得昂贵。
<强> 2)强>
NodeList
是一个“实时”列表意味着它被更新并引用当前位于文档树中的节点,而不是指最初构建列表时树中的节点或这些节点的克隆。这是掌握DOM初学者的重要特征。例如,如果您选择NodeList
兄弟元素并尝试向每个节点添加一个新的兄弟元素,则向item(i+1)
迈出一步将始终到达最新添加的节点,循环将永远不会完成。
第3)强> 实时列表还提供了一些解释为什么它被实现为链表(或AFAIK实际实现是双向链表)。在您的测试中可以清楚地看到这种效果,其中访问最后一个元素始终是最慢的,无论您是向后还是向前迭代它。
<强> 4)强> 由于缓存,如果缓存保持干净,则在不对树进行任何更改的情况下循环遍历单个列表应该是相当有效的。在某些Java版本中,此缓存存在问题。我没有调查所有程序使缓存无效但可能最安全的赌注是建议保持评估的表达式相同,不对树进行更改,一次循环一个列表,并始终步入下一个或上一个列表项。
当然,真正的性能胜利取决于用例。您应该尝试完全摆脱循环列表,而不是仅仅调整列表循环 - 至少是为了参考。克隆使列表无法生效。可以通过将节点复制到阵列来实现对节点的直接访问。如果结构合适,您还可以使用其他DOM方法,例如getNextSibling()
,它们表示比循环NodeList更有效。
答案 2 :(得分:5)
尝试克隆节点(因此您不会从其祖先那里获得不必要的引用)
Node singleNode = nodes.item(i).cloneNode(true);
如果删除子项,则会丢失引用,只会获得要处理的一半节点。
答案 3 :(得分:0)
这有点晚了,但我遇到了同样的情况,但似乎我的文件太大了,其他答案都没有真正解决问题。
最终,我找到了jaxen。一旦我使用它,以前需要15秒才能解析的文档只花了几毫秒。
不幸的是,Jaxen的记录非常糟糕,但效果很好:
DOMXPath myXPath = new DOMXPath("atom:id/text()");
String myContent = myXPath.stringValueOf(myDocument);
可以在http://jaxen.codehaus.org/apidocs/org/jaxen/dom/DOMXPath.html
找到Java Doc答案 4 :(得分:0)
每次从Nodelist中获取Node时,似乎它都会引用xml的整个结构;为此原因 当您导航节点时,xpath进程每次都从xml的根开始,因此,当您在trhee中进入时 这需要更多的时间。
因此,当您获取节点时,在导航之前,您必须通过此方法强制转换为字符串:
private String nodeToString(Node node) {
StringWriter sw = new StringWriter();
try {
Transformer t = TransformerFactory.newInstance().newTransformer();
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
t.transform(new DOMSource(node), new StreamResult(sw));
} catch (TransformerException te) {
System.out.println("nodeToString Transformer Exception");
}
return sw.toString();
}
然后在元素/节点中重新转换它:
String xml = nodeToString(node);
Element nodeNew = DocumentBuilderFactory
.newInstance()
.newDocumentBuilder()
.parse(new ByteArrayInputStream(xml.getBytes()))
.getDocumentElement();
node = nodeNew;
通过这种方式,新元素丢失了对其祖先的所有引用,并将用作简单节点而不是嵌套节点。 显然,只有当你必须深入到一个节点时,这种方法才是好的。