如果要告诉别人买一加仑牛奶,您会怎么说?“请去买一加仑牛奶回来” 还是 “从前门出去,向左转,走三个街区向右转,再走半个街区向右转进入商店。走向四号通道,沿通道走五米向左,拿一瓶一加仑装的牛奶然后到收银台付款。再沿原路回家。” 简直太可笑了。只要在 “请去买一加仑牛奶回来” 的基础上稍加指示,多数成人都能自己买回牛奶来。

查询语言和计算机搜索与此类似。直接说 “找一个 Cryptonomicon 的副本” 要比编写搜索某个数据库的详细逻辑容易得多。由于搜索操作的逻辑非常相似,可以发明一种通用语言让您使用 “找到 Neal Stephenson 的所有著作” 这样的命令,然后编写对特定数据存储执行此类查询的引擎。

XPath

在众多查询语言之中,结构化查询语言(SQL)是一种针对查询特定类型的关系库而设计和优化的语言。其他不那么常见的查询语言还有对象查询语言(OQL)和 XQuery。但本文的主题是 XPath,一种为查询 XML 文档而设计的查询语言。比如,下面这个简单的 XPath 查询可以在文档中找到作者为 Neal Stephenson 的所有图书的标题:

//book[author="Neal Stephenson"]/title

作为对照,查询同样信息的纯 DOM 搜索代码如 清单 1 所示:

清单 1. 找到 Neal Stephenson 所有著作 title 元素的 DOM 代码
        ArrayList result = new ArrayList();
NodeList books = doc.getElementsByTagName("book");
for (int i = 0; i < books.getLength(); i++) {
Element book = (Element) books.item(i);
NodeList authors = book.getElementsByTagName("author");
boolean stephenson = false;
for (int j = 0; j < authors.getLength(); j++) {
Element author = (Element) authors.item(j);
NodeList children = author.getChildNodes();
StringBuffer sb = new StringBuffer();
for (int k = 0; k < children.getLength(); k++) {
Node child = children.item(k);
// really should to do this recursively
if (child.getNodeType() == Node.TEXT_NODE) {
sb.append(child.getNodeValue());
}
}
if (sb.toString().equals("Neal Stephenson")) {
stephenson = true;
break;
} } if (stephenson) {
NodeList titles = book.getElementsByTagName("title");
for (int j = 0; j < titles.getLength(); j++) {
result.add(titles.item(j));
}
} }

不论您是否相信,清单 1 中的 DOM 显然不如简单的 XPath 表达式通用或者健壮。您愿意编写、调试和维护哪一个?我想答案很明显。

但是虽然有很强的表达能力,XPath 并不是 Java 语言,事实上 XPath 不是一种完整的编程语言。有很多东西用 XPath 表达不出来,甚至有些查询也无法表达。比方说,XPath 不能查找国际标准图书编码(ISBN)检验码不匹配的所有图书,或者找出境外帐户数据库显示欠帐的所有作者。幸运的是,可以把 XPath 结合到 Java 程序中,这样就能发挥两者的优势了:Java 做 Java 所擅长的,XPath 做 XPath 所擅长的。

直到最近,Java 程序执行 XPath 查询所需要的应用程序编程接口(API)还因形形色色的 XPath 引擎而各不相同。Xalan 有一种 API,Saxon 使用另一种,其他引擎则使用其他的 API。这意味着代码往往把您限制到一种产品上。理想情况下,最好能够试验具有不同性能特点的各种引擎,而不会带来不适当的麻烦或者重新编写代码。

于是,Java 5 推出了 javax.xml.xpath 包,提供一个引擎和对象模型独立的 XPath 库。这个包也可用于 Java 1.3 及以后的版本,但需要单独安装 Java API for XML Processing (JAXP) 1.3。Xalan 2.7 和 Saxon 8 以及其他产品包含了这个库的实现。

 

回页首

一个简单的例子

我将举例说明如何使用它。然后再讨论一些细节问题。假设要查询一个图书列表,寻找 Neal Stephenson 的著作。具体来说,这个图书列表的形式如 清单 2 所示:

清单 2. 包含图书信息的 XML 文档
<inventory>
<book year="2000">
<title>Snow Crash</title>
<author>Neal Stephenson</author>
<publisher>Spectra</publisher>
<isbn>0553380958</isbn>
<price>14.95</price>
</book> <book year="2005">
<title>Burning Tower</title>
<author>Larry Niven</author>
<author>Jerry Pournelle</author>
<publisher>Pocket</publisher>
<isbn>0743416910</isbn>
<price>5.99</price>
<book> <book year="1995">
<title>Zodiac</title>
<author>Neal Stephenson<author>
<publisher>Spectra</publisher>
<isbn>0553573862</isbn>
<price>7.50</price>
<book> <!-- more books... --> </inventory>

抽象工厂

XPathFactory 是一个抽象工厂。抽象工厂设计模式使得这一种 API 能够支持不同的对象模型,如 DOM、JDOM 和 XOM。为了选择不同的模型,需要向XPathFactory.newInstance() 方法传递标识对象模型的统一资源标识符(URI)。比如 http://xom.nu/ 可以选择 XOM。但实际上,到目前为止 DOM 是该 API 支持的惟一对象模型。

查找所有图书的 XPath 查询非常简单://book[author="Neal Stephenson"]。为了找出这些图书的标题,只要增加一步,表达式就变成了 //book[author="Neal Stephenson"]/title。最后,真正需要的是 title 元素的文本节点孩子。这就要求再增加一步,完整的表达式就是 //book[author="Neal Stephenson"]/title/text()

现在我提供一个简单的程序,它从 Java 语言中执行这个查询,然后把找到的所有图书的标题打印出来。首先,需要将文档加载到一个 DOM Document 对象中。为了简化起见,假设该文档在当前工作目录的 books.xml 文件中。下面的简单代码片段解析文档并建立对应的Document 对象:

清单 3. 用 JAXP 解析文档
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true); // never forget this!
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse("books.xml");

到目前为止,这仅仅是标准的 JAXP 和 DOM,没有什么新鲜的。

接下来创建 XPathFactory

XPathFactory factory = XPathFactory.newInstance();

然后使用这个工厂创建 XPath 对象:

XPath xpath = factory.newXPath();

XPath 对象编译 XPath 表达式:

PathExpression expr = xpath.compile("//book[author='Neal Stephenson']/title/text()");

直接求值

如果 XPath 表达式只使用一次,可以跳过编译步骤直接对 XPath 对象调用 evaluate() 方法。但是,如果同一个表达式要重复使用多次,编译可能更快一些。

最后,计算 XPath 表达式得到结果。表达式是针对特定的上下文节点计算的,在这个例子中是整个文档。还必须指定返回类型。这里要求返回一个节点集:

Object result = expr.evaluate(doc, XPathConstants.NODESET);

可以将结果强制转化成 DOM NodeList,然后遍历列表得到所有的标题:

        NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}

清单 4 把上述片段组合到了一个程序中。还要注意,这些方法可能抛出一些检查异常,这些异常必须在 throws 子句中声明,但是我在上面把它们掩盖起来了:

清单 4. 用固定的 XPath 表达式查询 XML 文档的完整程序
import java.io.IOException;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import javax.xml.parsers.*;
import javax.xml.xpath.*; public class XPathExample { public static void main(String[] args)
throws ParserConfigurationException, SAXException,
IOException, XPathExpressionException { DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true); // never forget this!
DocumentBuilder builder = domFactory.newDocumentBuilder();
Document doc = builder.parse("books.xml"); XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
XPathExpression expr
= xpath.compile("//book[author='Neal Stephenson']/title/text()"); Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
} } }

XPath 数据模型

每当混合使用诸如 XPath 和 Java 这样两种不同的语言时,必定会有某些将两者粘合在一起的明显接缝。并非一切都很合拍。XPath 和 Java 语言没有同样的类型系统。XPath 1.0 只有四种基本数据类型:

  • node-set
  • number
  • boolean
  • string

当然,Java 语言有更多的数据类型,包括用户定义的对象类型。

多数 XPath 表达式,特别是位置路径,都返回节点集。但是还有其他可能。比如,XPath 表达式 count(//book) 返回文档中的图书数量。XPath 表达式 count(//book[@author="Neal Stephenson"]) > 10 返回一个布尔值:如果文档中 Neal Stephenson 的著作超过 10 本则返回 true,否则返回 false。

evaluate() 方法被声明为返回 Object。实际返回什么依赖于 XPath 表达式的结果以及要求的类型。一般来说,XPath 的

  • number 映射为 java.lang.Double
  • string 映射为 java.lang.String
  • boolean 映射为 java.lang.Boolean
  • node-set 映射为 org.w3c.dom.NodeList

XPath 2

前面一直假设您使用的是 XPath 1.0。XPath 2 大大扩展和修改了类型系统。Java XPath API 支持 XPath 2 所需的主要修改是为返回 XPath 2 新数据类型增加常量。

在 Java 中计算 XPath 表达式时,第二个参数指定需要的返回类型。有五种可能,都在javax.xml.xpath.XPathConstants 类中命名了常量:

  • XPathConstants.NODESET
  • XPathConstants.BOOLEAN
  • XPathConstants.NUMBER
  • XPathConstants.STRING
  • XPathConstants.NODE

最后一个 XPathConstants.NODE 实际上没有匹配的 XPath 类型。只有知道 XPath 表达式只返回一个节点或者只需要一个节点时才使用它。如果 XPath 表达式返回了多个节点并且指定了 XPathConstants.NODE,则 evaluate() 按照文档顺序返回第一个节点。如果 XPath 表达式选择了一个空集并指定了 XPathConstants.NODE,则 evaluate() 返回 null。

如果不能完成要求的转换,evaluate() 将抛出 XPathException

 

回页首

名称空间上下文

若 XML 文档中的元素在名称空间中,查询该文档的 XPath 表达式必须使用相同的名称空间。XPath 表达式不一定要使用相同的前缀,只需要名称空间 URI 相同即可。事实上,如果 XML 文档使用默认名称空间,那么尽管目标文档没有使用前缀,XPath 表达式也必须使用前缀。

但是,Java 程序不是 XML 文档,因此不能用一般的名称空间解析。必须提供一个对象将前缀映射到名称空间 URI。该对象是javax.xml.namespace.NamespaceContext 接口的实例。比如,假设图书文档放在 http://www.example.com/books 名称空间中,如 清单 5 所示:

清单 5. 使用默认名称空间的 XML 文档
<inventory xmlns="http://www.example.com/books">
<book year="2000">
<title>Snow Crash</title>
<author>Neal Stephenson</author>
<publisher>Spectra</publisher>
<isbn>0553380958</isbn>
<price>14.95<price>
</book> <!-- more books... --> <inventory>

查找 Neal Stephenson 全部著作标题的 XPath 表达式就要改为 //pre:book[pre:author="Neal Stephenson"]/pre:title/text()。但是,必须将前缀 pre 映射到 URI http://www.example.com/books。NamespaceContext 接口在 Java 软件开发工具箱(JDK)或 JAXP 中没有默认实现似乎有点笨,但确实如此。不过,自己实现也不难。清单 6 对一个名称空间给出了简单的实现。还需要映射 xml 前缀。

清单 6. 绑定一个名称空间和默认名称空间的简单上下文
import java.util.Iterator;
import javax.xml.*;
import javax.xml.namespace.NamespaceContext; public class PersonalNamespaceContext implements NamespaceContext { public String getNamespaceURI(String prefix) {
if (prefix == null) throw new NullPointerException("Null prefix");
else if ("pre".equals(prefix)) return "http://www.example.org/books";
else if ("xml".equals(prefix)) return XMLConstants.XML_NS_URI;
return XMLConstants.NULL_NS_URI;
} // This method isn't necessary for XPath processing.
public String getPrefix(String uri) {
throw new UnsupportedOperationException();
} // This method isn't necessary for XPath processing either.
public Iterator getPrefixes(String uri) {
throw new UnsupportedOperationException();
} }

使用映射存储绑定和增加 setter 方法实现名称空间上下文的重用也不难。

创建 NamespaceContext 对象后,在编译表达式之前将其安装到 XPath 对象上。以后就可以像以前一样是用这些前缀查询了。比如:

清单 7. 使用名称空间的 XPath 查询
  XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
xpath.setNamespaceContext(new PersonalNamespaceContext());
XPathExpression expr
= xpath.compile("//pre:book[pre:author='Neal Stephenson']/pre:title/text()"); Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}
 

回页首

函数求解器

有时候,在 Java 语言中定义用于 XPath 表达式的扩展函数很有用。这些函数可以执行用纯 XPath 很难或者无法执行的任务。不过必须是真正的函数,而不是随意的方法。就是说不能有副作用。(XPath 函数可以按照任意的顺序求值任意多次。)

通过 Java XPath API 访问的扩展函数必须实现 javax.xml.xpath.XPathFunction 接口。这个接口只声明了一个方法 evaluate:

public Object evaluate(List args) throws XPathFunctionException

该方法必须返回 Java 语言能够转换到 XPath 的五种类型之一:

  • String
  • Double
  • Boolean
  • Nodelist
  • Node

比如,清单 8 显示了一个扩展函数,它检查 ISBN 的校验和并返回 Boolean。这个校验和的基本规则是前九位数的每一位乘上它的位置(即第一位数乘上 1,第二位数乘上 2,依次类推)。将这些数加起来然后取除以 11 的余数。如果余数是 10,那么最后一位数就是 X。

清单 8. 检查 ISBN 的 XPath 扩展函数
import java.util.List;
import javax.xml.xpath.*;
import org.w3c.dom.*; public class ISBNValidator implements XPathFunction { // This class could easily be implemented as a Singleton. public Object evaluate(List args) throws XPathFunctionException { if (args.size() != 1) {
throw new XPathFunctionException("Wrong number of arguments to valid-isbn()");
} String isbn;
Object o = args.get(0); // perform conversions
if (o instanceof String) isbn = (String) args.get(0);
else if (o instanceof Boolean) isbn = o.toString();
else if (o instanceof Double) isbn = o.toString();
else if (o instanceof NodeList) {
NodeList list = (NodeList) o;
Node node = list.item(0);
// getTextContent is available in Java 5 and DOM 3.
// In Java 1.4 and DOM 2, you'd need to recursively
// accumulate the content.
isbn= node.getTextContent();
}
else {
throw new XPathFunctionException("Could not convert argument type");
} char[] data = isbn.toCharArray();
if (data.length != 10) return Boolean.FALSE;
int checksum = 0;
for (int i = 0; i < 9; i++) {
checksum += (i+1) * (data[i]-'0');
}
int checkdigit = checksum % 11; if (checkdigit + '0' == data[9] || (data[9] == 'X' && checkdigit == 10)) {
return Boolean.TRUE;
}
return Boolean.FALSE; } }

下一步让这个扩展函数能够在 Java 程序中使用。为此,需要在编译表达式之前向 XPath 对象安装javax.xml.xpath.XPathFunctionResolver。函数求解器将函数的 XPath 名称和名称空间 URI 映射到实现该函数的 Java 类。清单 9 是一个简单的函数求解器,将扩展函数 valid-isbn 和名称空间 http://www.example.org/books 映射到 清单 8 中的类。比如,XPath 表达式//book[not(pre:valid-isbn(isbn))] 可以找到 ISBN 校验和不匹配的所有图书。

清单 9. 识别 valid-isbn 扩展函数的上下文
iimport javax.xml.namespace.QName;
import javax.xml.xpath.*; public class ISBNFunctionContext implements XPathFunctionResolver { private static final QName name
= new QName("http://www.example.org/books", "valid-isbn"); public XPathFunction resolveFunction(QName name, int arity) {
if (name.equals(ISBNFunctionContext.name) && arity == 1) {
return new ISBNValidator();
}
return null;
} }

由于扩展函数必须有名称空间,所以计算包含扩展函数的表达式时必须使用 NamespaceResolver,即便查询的文档没有使用任何名称空间。由于 XPathFunctionResolverXPathFunction 和 NamespaceResolver 都是接口,如果方便的话可以将它们放在所有的类中。

 

回页首

结束语

用 SQL 和 XPath 这样的声明性语言编写查询,要比使用 Java 和 C 这样的命令式语言容易得多。但是,用 Java 和 C 这样的图灵完整语言编写复杂的逻辑,又比 SQL 和 XPath 这样的声明性语言容易得多。所幸的是,通过使用 Java Database Connectivity (JDBC) 和 javax.xml.xpath之类的 API 可以将两者结合起来。随着世界上越来越多的数据转向 XML,javax.xml.xpath 将与 java.sql 一样变得越来越重要。

Java 语言的 XPath API的更多相关文章

  1. JAVA语言规范和API网址

    Java语言规范: http://docs.oracle.com/javase/specs/ Java API: http://docs.oracle.com/javase/8/docs/api/in ...

  2. Java语言程序设计(基础篇)第一章

    第一章 计算机.程序和Java概述 1.1 引言 什么是程序设计呢? 程序设计就是创建(或者开发)软件,软件也称为程序. 1.2 什么是计算机 计算机是存储和处理数据的电子设备,计算机包括硬件(har ...

  3. JDK1.7新特性(3):java语言动态性之脚本语言API

    简要描述:其实在jdk1.6中就引入了支持脚本语言的API.这使得java能够很轻松的调用其他脚本语言.具体API的使用参考下面的代码: package com.rampage.jdk7.chapte ...

  4. 已看1.熟练的使用Java语言进行面向对象程序设计,有良好的编程习惯,熟悉常用的Java API,包括集合框架、多线程(并发编程)、I/O(NIO)、Socket、JDBC、XML、反射等。[泛型]\

    1.熟练的使用Java语言进行面向对象程序设计,有良好的编程习惯,熟悉常用的Java API,包括集合框架.多线程(并发编程).I/O(NIO).Socket.JDBC.XML.反射等.[泛型]\1* ...

  5. java语言进阶(一)_Object类_常用API

    第一章 Object类 1.1 概述 java.lang.Object类是Java语言中的根类,即所有类的父类.它中描述的所有方法子类都可以使用.在对象实例化的时候,最终找的父类就是Object. 如 ...

  6. 在Java中使用xpath对xml解析

    xpath是一门在xml文档中查找信息的语言.xpath用于在XML文档中通过元素和属性进行导航.它的返回值可能是节点,节点集合,文本,以及节点和文本的混合等.在学习本文档之前应该对XML的节点,元素 ...

  7. 【百度文库课程】Java语言基础与OOP入门学习笔记一

    一. Java的历史与由来 原名Oak,针对嵌入式系统开发设计,语法与C/C++基本一致 二. Java语言特点 Java由四方面组成:Java编程语言.Java类文件格式.Java虚拟机和Java应 ...

  8. 0031 Java学习笔记-梁勇著《Java语言程序设计-基础篇 第十版》英语单词

    第01章 计算机.程序和Java概述 CPU(Central Processing Unit) * 中央处理器 Control Unit * 控制单元 arithmetic/logic unit /ə ...

  9. 20145205 java语言实现数据结构实验一

    数据结构实验要求 综合类实验设计3 已知有一组数据a1a2a3a4--anb1b2b3b4--bm,其中ai均大于bj,但是a1到an和b1到bm不是有序的,试设计两到三个算法完成数据排序,且把bj数 ...

随机推荐

  1. STM32通过FSMC驱动3.2寸液晶屏实现的音乐频谱

    视频演示: http://player.youku.com/player.php/sid/XNDcyMDgwMTE2/v.swf 源码下载: lattice_ music _tft.rar(1.42 ...

  2. C++中#和##的特殊使用

    1.用#号将输入的内容转换为字符串. 用##号将两个参数合并. #include <iostream> using namespace std; //将输入的内容转换成字符串 #defin ...

  3. Java基础篇Socket网络编程中的应用实例

    说到java网络通讯章节的内容,刚入门的学员可能会感到比较头疼,应为Socket通信中一定会伴随有IO流的操作,当然对IO流比较熟练的哥们会觉得这是比较好玩的一章,因为一切都在他们的掌握之中,这样操作 ...

  4. 背水一战 Windows 10 (38) - 控件(布局类): Panel, Canvas, RelativePanel, StackPanel, Grid

    [源码下载] 背水一战 Windows 10 (38) - 控件(布局类): Panel, Canvas, RelativePanel, StackPanel, Grid 作者:webabcd 介绍背 ...

  5. centos 安装apache activemq

    安装说明 安装环境:CentOS-6.3 安装方式:源码安装  软件:apache-tomcat-7.0.29.tar.gz下载地址http://activemq.apache.org/downloa ...

  6. IMAX公司CEO:进军VR产业,打造VR体验中心

    591ARVR资讯网www.591arvr.com报道近日,<财富>杂志采访了IMAX首席执行官理查德·葛尔方(Richar Gelfond),后者谈了谈虚拟现实和IMAX正在打造的VR影 ...

  7. C#中(int)a和Convert.ToInt32(a)的区别

    首先,在 C# 中,int 其实就是 System.Int32,即都是32位的. 其次,(int) 和 Convert.ToInt32 是两个不同的概念,前者是类型转换,而后者则是内容转换,它们并不总 ...

  8. UGUI事件系统

    UGUI系统 将UI可能触发的事件分为12个类型,即EventTriggerType枚举的12个值. PointerEnter-- PointerExit-- PointerDown-- Pointe ...

  9. iOS-延迟操作方法总结

    在实际应用中,有时候我们会需要延时执行某些操作,所以我们这里总结了四种延迟操作的方法,并简要分析了每种方法的异同. NSObject的相关方法 第一种方法是使用NSObject类的performSel ...

  10. 使用Nginx+Lua(OpenResty)开发高性能Web应用

    摘自(http://jinnianshilongnian.iteye.com/blog/2280928) 在互联网公司,Nginx可以说是标配组件,但是主要场景还是负载均衡.反向代理.代理缓存.限流等 ...