XPath 是專為 XML 文檔設計的查詢語言,Java 5+ 內建 javax.xml.xpath 包提供引擎無關的標準 API。本文詳細講解五種回傳類型、命名空間上下文、編譯緩存、異常處理與效能優化。
XPathFactory:工廠,newInstance() 取得預設實作 (Sun/Oracle 內建 XPath 1.0 引擎)。XPath:編譯表達式、執行查詢、設定命名空間上下文。XPathExpression:預編譯表達式,線程安全,可重複執行。XPathConstants:五種回傳類型常量。| 常量 | Java 類型 | 適用 XPath 結果 | 典型用法 |
|---|---|---|---|
NODESET | NodeList | 多節點集合 | //book, //title/text() |
NODE | Node (單節點) | 單一元素/屬性 | /bookstore/book[1], //@id |
STRING | String | 字串值 (自動串聯多節點文字) | string(//book[1]/title) |
NUMBER | Double | 數值 (自動轉換) | count(//book), sum(//price) |
BOOLEAN | Boolean | 真/假 | boolean(//book[@category='WEB']) |
import javax.xml.xpath.*;
import org.w3c.dom.*;
import javax.xml.parsers.*;
import java.io.File;
public class XPathDemo {
public static void main(String[] args) throws Exception {
// 1. 載入 XML 文檔到 DOM 樹
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = builder.parse(new File("books.xml"));
// 2. 建立 XPath 物件
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
// 3. 註冊命名空間 (若 XML 有命名空間,必須實作 NamespaceContext)
// xpath.setNamespaceContext(new MyNamespaceContext());
// 4. 編譯表達式 (重用 XPathExpression)
XPathExpression exprBook = xpath.compile("//book[@category='WEB']");
XPathExpression exprTitle = xpath.compile("title/text()");
XPathExpression exprCount = xpath.compile("count(//book)");
// 5. 執行查詢 - 示範五種回傳類型
// NODESET -> NodeList
NodeList books = (NodeList) exprBook.evaluate(doc, XPathConstants.NODESET);
System.out.println("WEB 類書籍數: " + books.getLength());
for (int i = 0; i < books.getLength(); i++) {
Element book = (Element) books.item(i);
// 相對當前節點查詢
String title = (String) exprTitle.evaluate(book, XPathConstants.STRING);
System.out.println(" - " + title);
}
// NODE -> 單一元素
Element firstBook = (Element) xpath.compile("/bookstore/book[1]")
.evaluate(doc, XPathConstants.NODE);
System.out.println("第一本書: " + firstBook.getElementsByTagName("title").item(0).getTextContent());
// STRING -> 直接取文字
String price = (String) xpath.compile("string(//book[@category='COOKING']/price)")
.evaluate(doc, XPathConstants.STRING);
System.out.println("烹飲類價格: " + price);
// NUMBER -> 計數/求和
Double count = (Double) exprCount.evaluate(doc, XPathConstants.NUMBER);
Double sum = (Double) xpath.compile("sum(//price)").evaluate(doc, XPathConstants.NUMBER);
System.out.println("總冊數: " + count.intValue() + ", 總價: " + sum);
// BOOLEAN -> 存在性判斷
Boolean hasJava = (Boolean) xpath.compile("boolean(//book[contains(title,'Java')])")
.evaluate(doc, XPathConstants.BOOLEAN);
System.out.println("是否有 Java 相關書: " + hasJava);
}
}
XML 若宣告 xmlns="..." 或 xmlns:ns="...",XPath 必須 註冊前綴映射,否則查無結果。
import javax.xml.namespace.NamespaceContext;
import java.util.*;
public class SimpleNamespaceContext implements NamespaceContext {
private final Map<String, String> nsMap;
public SimpleNamespaceContext(Map<String, String> nsMap) { this.nsMap = nsMap; }
@Override public String getNamespaceURI(String prefix) { return nsMap.get(prefix); }
@Override public String getPrefix(String uri) { /* 反向查找略 */ return null; }
@Override public Iterator<String> getPrefixes(String uri) { return Collections.emptyIterator(); }
}
// 使用
Map<String, String> ns = new HashMap<>();
ns.put("bk", "https://example.com/bookstore");
xpath.setNamespaceContext(new SimpleNamespaceContext(ns));
// 現在可用 //bk:book 查詢
compile() 開銷較大,靜態編譯一次、多次 evaluate()。XMLStreamReader 逐塊處理 (需第三方庫如 Saxon 支援流式 XPath)。DocumentBuilderFactory 設定 setFeature("https://apache.org/xml/features/disallow-doctype-decl", true) 防 XXE 攻擊。evaluateAsString(), evaluateAsNodeList() 等輔助方法,避免到處強轉。XPathExpressionException:語法錯誤、命名空間未註冊、表達式對當前文檔無效。NullPointerException:evaluate() 回傳 null 時強轉 (如 NODE 無匹配)。請先用 BOOLEAN 判斷存在性。ClassCastException:請求類型與實際結果不符 (如 NODESET 強轉 Node)。