Apache Commons JXPath 1.4.0 快速入门与集成示例
JXPath 1.4.0 快速入门:Maven 依赖引入、Bean 对象图与 XML DOM 的 XPath 导航完整示例、官方二进制包免 Maven 验证与业务集成方式,代码完整可直接复制,输出全部实测。
Apache Commons JXPath 1.4.0 快速入门与集成示例
目标:用最短路径跑通 Apache Commons JXPath 1.4.0——从 Maven 引入依赖,到 Bean 对象图导航、XML DOM 导航的真实运行验证,再到免 Maven 的二进制包验证与业务工程集成。示例代码完整可直接复制,全部输出均在 Ubuntu 22.04(x86_64,JDK 17.0.20.1 + Maven 3.6.3)实测通过。
一、环境信息
本节先固定本次实测所用软硬件基线,便于读者逐项对照复现。
- 操作系统:Ubuntu 22.04(x86_64)
- JDK:OpenJDK 17.0.20.1(
java -version实测,回显见 4.2 前置条件) - 构建工具:Apache Maven 3.6.3(
mvn -v实测,回显见 4.2 前置条件) - 目标库:Apache Commons JXPath 1.4.0(Maven 坐标
commons-jxpath:commons-jxpath:1.4.0,旧 groupId) - 依赖来源:Maven 中央仓库(
repo.maven.apache.org)与官方发行目录(downloads.apache.org),网络直连可达 - Maven 示例工程目录:
/opt/jxpath-demo - 官方二进制包解压目录:
/opt/jxpath-dist - 二进制包演示目录:
/opt/jxpath-dist-demo
二、前置条件
确认 JDK 与 Maven 可用,并创建本文统一的示例工程目录:
java -version 2>&1; mvn -v | head -3; mkdir -p /opt/jxpath-demo && echo DIR_OK
输出:
openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment (build 17.0.20.1+1-1-22.04-Ubuntu)
OpenJDK 64-Bit Server VM (build 17.0.20.1+1-1-22.04-Ubuntu, mixed mode, sharing)
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 17.0.20.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64
DIR_OK
解读:JDK 17.0.20.1、Maven 3.6.3(跟随系统 JDK 17)、DIR_OK 确认示例目录已创建。若尚未安装 JDK/Maven,按《Ubuntu 22.04 安装 Apache Commons JXPath 指南》第二章安装;CentOS 8 用户注意多 JDK 并存问题(JAVA_HOME 与运行时路径),见《CentOS 8 安装 Apache Commons JXPath 指南》。
三、依赖引入
先把第五、六章的两个示例源码放入 src/main/java/demo/(BeanNav.java、XmlNav.java,完整内容见后文),再在 /opt/jxpath-demo/pom.xml 中引入 JXPath(坐标务必用旧 groupId,见下文说明)并编译核实:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>jxpath-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>commons-jxpath</groupId>
<artifactId>commons-jxpath</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
</plugins>
</build>
</project>
编译并核实依赖下载:
cd /opt/jxpath-demo && mvn -B compile 2>&1 | tail -30
输出节选(实测 tail -30):
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------< demo:jxpath-demo >--------------------------
[INFO] Building jxpath-demo 1.0.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/commons-jxpath/commons-jxpath/1.4.0/commons-jxpath-1.4.0.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/commons-jxpath/commons-jxpath/1.4.0/commons-jxpath-1.4.0.pom (14 kB at 9.7 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/81/commons-parent-81.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/81/commons-parent-81.pom (78 kB at 148 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/apache/33/apache-33.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/apache/33/apache-33.pom (24 kB at 16 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/junit/junit-bom/5.11.4/junit-bom-5.11.4.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/junit/junit-bom/5.11.4/junit-bom-5.11.4.pom (5.6 kB at 11 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/commons-jxpath/commons-jxpath/1.4.0/commons-jxpath-1.4.0.jar
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/commons-jxpath/commons-jxpath/1.4.0/commons-jxpath-1.4.0.jar (329 kB at 176 kB/s)
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ jxpath-demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /opt/jxpath-demo/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.11.0:compile (default-compile) @ jxpath-demo ---
[INFO] Changes detected - recompiling the module! :dependency
[INFO] Compiling 2 source files with javac [debug release 17] to target/classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.138 s
[INFO] Finished at: 2026-10-14T10:23:36Z
[INFO] ------------------------------------------------------------------------
解读下载日志:真正落地的依赖 jar 只有 commons-jxpath-1.4.0.jar(329 kB)一个,其余 pom(commons-parent 81、apache 33、junit-bom)是父 POM 链上的元数据文件。再单独核实依赖树(确认无传递依赖):
cd /opt/jxpath-demo && mvn -B dependency:tree 2>&1 | sed -n "/maven-dependency-plugin/,\$p"
输出:
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ jxpath-demo ---
[INFO] demo:jxpath-demo:jar:1.0.0
[INFO] \- commons-jxpath:commons-jxpath:jar:1.4.0:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.731 s
[INFO] Finished at: 2026-10-14T10:23:45Z
[INFO] ------------------------------------------------------------------------
两点关键信息:依赖树只有 JXPath 自身一行——1.4.0 无强制传递依赖;下载日志里真正落地的依赖 jar 只有 commons-jxpath-1.4.0.jar(329 kB)一个,其余 pom(commons-parent 81、apache 33、junit-bom)是父 POM 链元数据,不构成运行期依赖。坐标陷阱再强调:org.apache.commons:commons-jxpath 在中央仓库不存在(404),必须写 commons-jxpath:commons-jxpath:1.4.0。
再生成运行期 classpath 文件(后续直跑示例用):
cd /opt/jxpath-demo && mvn -B dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt -q 2>&1 | tail -3; echo CP=$(cat /tmp/cp.txt)
输出:
CP=/root/.m2/repository/commons-jxpath/commons-jxpath/1.4.0/commons-jxpath-1.4.0.jar
CP= 行证明运行期只需要这一个 jar。
四、核心概念速览
JXPath 把 XPath 1.0 表达式求值从 XML 扩展到 Java 对象图:JXPathContext.newContext(任意 JavaBean 或 DOM Document) 后,同一个 getValue/iterate/setValue API 就能沿 getter/setter 链或 DOM 树导航。语法映射关系速查:
| XPath 写法 | Bean 上下文含义 | XML 上下文含义 |
|---|---|---|
name | 调用 getName() | 子元素 <name> |
employees[1] | List 第 1 个成员(1-based) | 第 1 个同名子元素(1-based) |
employees[age > 30] | 按属性谓词过滤集合元素 | 按子元素谓词过滤节点 |
price/@currency | 无此形态(属性即 getter) | 元素的 XML 属性 |
$minAge | 上下文变量(declareVariable 声明) | 同左 |
util:reverse(name) | setFunctions 注册的扩展函数 | 同左 |
💡 技术拓展:「对象图导航/XPath 求值」在 Java 生态不止 JXPath 一种实现。常见选项对比——
库 导航模型 特点 适用场景 Commons JXPath XPath 1.0 同时作用于 JavaBean 对象图与 XML DOM 一套语法通吃 Bean 属性/集合与 XML;支持 Pointer、路径创建、变量与扩展函数 需要用同一表达式语法访问 JavaBean 和 XML Jaxen 独立 XPath 引擎,供 dom4j/JDOM/XOM 等树模型适配 XPath 引擎本体,不含 Bean 导航;常作为 dom4j 的底层 已用 dom4j/JDOM 处理 XML,只需 XPath javax.xml.xpath(JDK 内置) JDK 自带 XPath 1.0,面向 DOM/Source 零依赖,但只支持 XML,不支持 JavaBean 导航 仅 XML 求值、不想引第三方库 Commons BeanUtils / PropertyUtils 属性路径字符串( a.b[0].c)按 getter/setter 反射轻量嵌套属性读写,但不支持谓词过滤、函数、XPath 轴 简单嵌套属性取值/赋值、对象属性拷贝 Spring EL(SpEL) 表达式语言,支持属性导航、集合选择投影 功能强但绑定 Spring 体系,表达式非 XPath 方言 Spring 技术栈内的动态取值与条件求值 本文场景需要「同一套 XPath 语法既访问 JavaBean 对象图又访问 XML DOM」,采用 Commons JXPath 1.4.0:无传递依赖、单 jar 即可运行,1.4.0 已于 2025 年恢复维护。
五、示例 1:Bean 对象图导航
本示例构建 Company/Employee 两层对象图,覆盖基本取值、谓词过滤、集合遍历、Pointer、修改、按路径创建、变量与扩展函数八组用法。源码 /opt/jxpath-demo/src/main/java/demo/BeanNav.java 完整内容:
package demo;
import org.apache.commons.jxpath.AbstractFactory;
import org.apache.commons.jxpath.ClassFunctions;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.Pointer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* JXPath 1.4.0 Bean navigation demo.
*/
public class BeanNav {
public static class Address {
private String city;
public Address() {
}
public Address(String city) {
this.city = city;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
@Override
public String toString() {
return "Address{city='" + city + "'}";
}
}
public static class Employee {
private String name;
private int age;
private Address address;
public Employee(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public String toString() {
return "Employee{name='" + name + "', age=" + age + ", address=" + address + "}";
}
}
public static class Company {
private String name;
private List<Employee> employees = new ArrayList<>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Employee> getEmployees() {
return employees;
}
public void setEmployees(List<Employee> employees) {
this.employees = employees;
}
}
public static class UtilFunctions {
public static String reverse(String s) {
return new StringBuilder(s).reverse().toString();
}
public static int charCount(String s) {
return s.length();
}
}
public static void main(String[] args) {
Company company = new Company();
company.setName("Acme Corp");
company.getEmployees().add(new Employee("Alice", 28, new Address("Beijing")));
company.getEmployees().add(new Employee("Bob", 35, new Address("Shanghai")));
company.getEmployees().add(new Employee("Carol", 41, null));
JXPathContext ctx = JXPathContext.newContext(company);
System.out.println("== 1. getValue basic & predicate ==");
System.out.println("getValue(\"name\") = " + ctx.getValue("name"));
try {
System.out.println("getValue(\"employees[1].name\") = " + ctx.getValue("employees[1].name"));
} catch (Exception e) {
System.out.println("getValue(\"employees[1].name\") threw " + e.getClass().getName());
System.out.println(" message: " + e.getMessage());
}
System.out.println("getValue(\"employees[1]/name\") = " + ctx.getValue("employees[1]/name"));
System.out.println("getValue(\"employees[age > 30]/name\") = " + ctx.getValue("employees[age > 30]/name"));
System.out.println();
System.out.println("== 2. iterate() over /employees ==");
Iterator<?> it = ctx.iterate("/employees");
while (it.hasNext()) {
System.out.println(" iterate -> " + it.next());
}
System.out.println();
System.out.println("== 3. iteratePointers() employees/address/city ==");
Iterator<?> ptrIt = ctx.iteratePointers("employees/address/city");
while (ptrIt.hasNext()) {
Pointer p = (Pointer) ptrIt.next();
System.out.println(" path=" + p.asPath() + " value=" + p.getValue());
}
System.out.println();
System.out.println("== 4. setValue (XPath index is 1-based: employees[2] = Bob) ==");
ctx.setValue("employees[2]/age", 36);
System.out.println(" after setValue(employees[2]/age, 36) -> " + company.getEmployees().get(1));
System.out.println();
System.out.println("== 5. createPathAndSetValue (auto create null path) ==");
try {
ctx.createPathAndSetValue("employees[name = 'Carol']/address/city", "Shenzhen");
System.out.println(" Carol now -> " + company.getEmployees().get(2));
} catch (Exception e) {
System.out.println(" without AbstractFactory threw " + e.getClass().getName());
Throwable c = e;
while (c.getCause() != null) {
c = c.getCause();
}
System.out.println(" root cause: " + c.getMessage());
ctx.setFactory(new AbstractFactory() {
@Override
public boolean createObject(JXPathContext context, Pointer pointer, Object parent,
String name, int index) {
if (parent instanceof Employee && "address".equals(name)) {
((Employee) parent).setAddress(new Address());
return true;
}
return false;
}
});
ctx.createPathAndSetValue("employees[name = 'Carol']/address/city", "Shenzhen");
System.out.println(" with AbstractFactory, Carol now -> " + company.getEmployees().get(2));
}
System.out.println();
System.out.println("== 6. declareVariable & $var ==");
ctx.getVariables().declareVariable("minAge", Integer.valueOf(30));
Iterator<?> seniors = ctx.iterate("employees[age > $minAge]/name");
while (seniors.hasNext()) {
System.out.println(" senior = " + seniors.next());
}
System.out.println();
System.out.println("== 7. custom functions via ClassFunctions ==");
ctx.setFunctions(new ClassFunctions(UtilFunctions.class, "util"));
System.out.println(" util:reverse(name) = " + ctx.getValue("util:reverse(name)"));
System.out.println(" util:charCount(name) = " + ctx.getValue("util:charCount(name)"));
System.out.println();
System.out.println("== 8. selectNodes on bean context ==");
List<?> names = ctx.selectNodes("employees[age > 30]/name");
System.out.println(" selectNodes(employees[age > 30]/name).size() = " + names.size() + ", names = " + names);
}
}
编译并运行:
cd /opt/jxpath-demo && mvn -B dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt -q 2>&1 | tail -3; echo CP=$(cat /tmp/cp.txt); java -cp target/classes:$(cat /tmp/cp.txt) demo.BeanNav 2>&1
输出(-q 静默模式下 build-classpath 无多余输出,CP= 即运行期 classpath):
CP=/root/.m2/repository/commons-jxpath/commons-jxpath/1.4.0/commons-jxpath-1.4.0.jar
== 1. getValue basic & predicate ==
getValue("name") = Acme Corp
getValue("employees[1].name") threw org.apache.commons.jxpath.JXPathInvalidSyntaxException
message: Invalid XPath: 'employees[1].name'. Syntax error after: 'employees[1]'
getValue("employees[1]/name") = Alice
getValue("employees[age > 30]/name") = Bob
== 2. iterate() over /employees ==
iterate -> Employee{name='Alice', age=28, address=Address{city='Beijing'}}
iterate -> Employee{name='Bob', age=35, address=Address{city='Shanghai'}}
iterate -> Employee{name='Carol', age=41, address=null}
== 3. iteratePointers() employees/address/city ==
path=/employees[1]/address/city value=Beijing
path=/employees[2]/address/city value=Shanghai
== 4. setValue (XPath index is 1-based: employees[2] = Bob) ==
after setValue(employees[2]/age, 36) -> Employee{name='Bob', age=36, address=Address{city='Shanghai'}}
== 5. createPathAndSetValue (auto create null path) ==
without AbstractFactory threw org.apache.commons.jxpath.JXPathException
root cause: Factory is not set on the JXPathContext - cannot create path: /employees[3]/address
with AbstractFactory, Carol now -> Employee{name='Carol', age=41, address=Address{city='Shenzhen'}}
== 6. declareVariable & $var ==
senior = Bob
senior = Carol
== 7. custom functions via ClassFunctions ==
util:reverse(name) = proC emcA
util:charCount(name) = 9
== 8. selectNodes on bean context ==
selectNodes(employees[age > 30]/name).size() = 2, names = [Bob, Carol]
结果校验(8 组全部符合预期):第 1 组确认点式路径报错、/ 续写与谓词过滤正常;第 2 组遍历出全部 3 个对象;第 3 组 Pointer 给出规范路径且跳过 null 的 address 分支;第 4 组 1-based 下标把 Bob 的 age 改为 36;第 5 组证明按路径创建必须先 setFactory;第 6 组变量过滤命中 Bob/Carol;第 7 组扩展函数把 Acme Corp 反转为 proC emcA、长度 9;第 8 组 selectNodes 返回值列表 [Bob, Carol]。逐类语法解读见《Apache Commons JXPath 1.4.0 常用 API 与 XPath 语法详解》。
六、示例 2:XML DOM 导航
本示例用同一种 JXPathContext API 导航一段内嵌 XML(根元素 library 带命名空间 xmlns:d="http://demo/ns/discount",shelf id="A" 下 3 个 book,第一本带 d:discount 子元素 0.8,三本价格均为 USD 计价的 39.99/32.50/45.00)。源码 /opt/jxpath-demo/src/main/java/demo/XmlNav.java 完整内容:
package demo;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.Pointer;
import org.w3c.dom.Document;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
/**
* JXPath 1.4.0 XML (DOM) navigation demo.
*/
public class XmlNav {
private static final String XML =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<library xmlns:d=\"http://demo/ns/discount\">\n"
+ " <shelf id=\"A\">\n"
+ " <book>\n"
+ " <title>Java Concurrency in Practice</title>\n"
+ " <author>Brian Goetz</author>\n"
+ " <price currency=\"USD\">39.99</price>\n"
+ " <d:discount>0.8</d:discount>\n"
+ " </book>\n"
+ " <book>\n"
+ " <title>Clean Code</title>\n"
+ " <author>Robert C. Martin</author>\n"
+ " <price currency=\"USD\">32.50</price>\n"
+ " </book>\n"
+ " <book>\n"
+ " <title>Effective Java</title>\n"
+ " <author>Joshua Bloch</author>\n"
+ " <price currency=\"USD\">45.00</price>\n"
+ " </book>\n"
+ " </shelf>\n"
+ "</library>\n";
public static void main(String[] args) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
Document doc = dbf.newDocumentBuilder()
.parse(new ByteArrayInputStream(XML.getBytes(StandardCharsets.UTF_8)));
JXPathContext ctx = JXPathContext.newContext(doc);
System.out.println("== 1. getValue absolute path ==");
System.out.println(" /library/shelf/book[1]/title = " + ctx.getValue("/library/shelf/book[1]/title"));
System.out.println(" /library/shelf/book/author = " + ctx.getValue("/library/shelf/book/author"));
System.out.println();
System.out.println("== 2. selectNodes(\"//book/title\") ==");
List<?> titles = ctx.selectNodes("//book/title");
for (Object t : titles) {
System.out.println(" item class = " + t.getClass().getName() + ", toString = " + t);
if (t instanceof Pointer) {
Pointer p = (Pointer) t;
System.out.println(" asPath=" + p.asPath() + " value=" + p.getValue());
}
if (t instanceof org.w3c.dom.Element) {
System.out.println(" as Element, getTextContent() = " + ((org.w3c.dom.Element) t).getTextContent());
}
}
System.out.println();
System.out.println("== 3. iteratePointers(\"//book\") with asPath ==");
Iterator<?> pit = ctx.iteratePointers("//book");
while (pit.hasNext()) {
Pointer p = (Pointer) pit.next();
System.out.println(" " + p.asPath());
}
System.out.println();
System.out.println("== 4. predicate / attribute / function ==");
System.out.println(" //book[price > 40]/title = " + ctx.getValue("//book[price > 40]/title"));
System.out.println(" //book[2]/price/@currency = " + ctx.getValue("//book[2]/price/@currency"));
System.out.println(" sum(//book/price) = " + ctx.getValue("sum(//book/price)"));
System.out.println();
System.out.println("== 5. namespace handling ==");
System.out.println(" before register: //book/d:discount = " + ctx.getValue("//book/d:discount"));
ctx.registerNamespace("d", "http://demo/ns/discount");
System.out.println(" after register: //book/d:discount = " + ctx.getValue("//book/d:discount"));
System.out.println();
System.out.println("== 6. relative context from a Pointer ==");
Pointer bookPtr = ctx.getPointer("//book[2]");
try {
JXPathContext viaValue = JXPathContext.newContext(ctx, bookPtr.getValue());
System.out.println(" via getValue(): title = " + viaValue.getValue("title"));
} catch (Exception e) {
System.out.println(" via getValue() threw " + e.getClass().getName());
System.out.println(" message: " + e.getMessage());
}
JXPathContext relCtx = JXPathContext.newContext(ctx, bookPtr.getNode());
System.out.println(" via getNode(): title = " + relCtx.getValue("title"));
System.out.println(" via getNode(): price/@currency = " + relCtx.getValue("price/@currency"));
}
}
运行(demo.BeanNav 与 demo.XmlNav 已在第三章 mvn -B compile 时一并编译):
cd /opt/jxpath-demo && java -cp target/classes:$(cat /tmp/cp.txt) demo.XmlNav 2>&1
输出:
== 1. getValue absolute path ==
/library/shelf/book[1]/title = Java Concurrency in Practice
/library/shelf/book/author = Brian Goetz
== 2. selectNodes("//book/title") ==
item class = com.sun.org.apache.xerces.internal.dom.DeferredElementNSImpl, toString = [title: null]
as Element, getTextContent() = Java Concurrency in Practice
item class = com.sun.org.apache.xerces.internal.dom.DeferredElementNSImpl, toString = [title: null]
as Element, getTextContent() = Clean Code
item class = com.sun.org.apache.xerces.internal.dom.DeferredElementNSImpl, toString = [title: null]
as Element, getTextContent() = Effective Java
== 3. iteratePointers("//book") with asPath ==
/library[1]/shelf[1]/book[1]
/library[1]/shelf[1]/book[2]
/library[1]/shelf[1]/book[3]
== 4. predicate / attribute / function ==
//book[price > 40]/title = Effective Java
//book[2]/price/@currency = USD
sum(//book/price) = 117.49000000000001
== 5. namespace handling ==
before register: //book/d:discount = 0.8
after register: //book/d:discount = 0.8
== 6. relative context from a Pointer ==
via getValue() threw org.apache.commons.jxpath.JXPathNotFoundException
message: No value for xpath: 'Clean CodeRobert C. Martin32.50'/title
via getNode(): title = Clean Code
via getNode(): price/@currency = USD
结果校验(6 组全部符合预期):绝对路径与 1-based 下标正常;selectNodes 返回的是 JDK 内置 Xerces 的 Element(DeferredElementNSImpl),循环里的 instanceof Pointer 分支未命中(结果不是 Pointer),取文本要 getTextContent();iteratePointers 给出 3 条规范路径;谓词/属性/sum() 函数齐活(求和的尾数是 double 语义的正常浮点误差);d: 前缀在 namespace-aware DOM 下自动解析(第一本的 d:discount 未注册前缀即取到 0.8)、registerNamespace 后等价;相对上下文必须用 getNode()(getValue() 是文本聚合值,会抛 JXPathNotFoundException)。全程未引任何 XML 库,解析走 JDK 内置实现。
七、免 Maven 快速验证(官方二进制包)
没有 Maven 环境时,用官方二进制包 + JDK 即可完成同样的验证。下载解压:
cd /opt && curl -sO https://downloads.apache.org/commons/jxpath/binaries/commons-jxpath-1.4.0-bin.tar.gz && ls -l commons-jxpath-1.4.0-bin.tar.gz && mkdir -p /opt/jxpath-dist && tar xzf commons-jxpath-1.4.0-bin.tar.gz -C /opt/jxpath-dist --strip-components=1 && find /opt/jxpath-dist -name "*.jar" | sort
输出:
-rw-r--r-- 1 root root 3374636 Oct 14 10:33 commons-jxpath-1.4.0-bin.tar.gz
/opt/jxpath-dist/commons-jxpath-1.4.0-javadoc.jar
/opt/jxpath-dist/commons-jxpath-1.4.0-sources.jar
/opt/jxpath-dist/commons-jxpath-1.4.0-test-sources.jar
/opt/jxpath-dist/commons-jxpath-1.4.0-tests.jar
/opt/jxpath-dist/commons-jxpath-1.4.0.jar
再写最小程序 /opt/jxpath-dist-demo/Main.java(无包名,Bean 导航四连取):
import org.apache.commons.jxpath.JXPathContext;
import java.util.ArrayList;
import java.util.List;
/**
* Minimal demo for the official binary distribution (commons-jxpath-1.4.0-bin.tar.gz).
* Compiled and run with plain javac/java + -cp.
*/
public class Main {
public static class Address {
private final String city;
public Address(String city) {
this.city = city;
}
public String getCity() {
return city;
}
}
public static class Employee {
private final String name;
private final int age;
private final Address address;
public Employee(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public Address getAddress() {
return address;
}
}
public static class Company {
private final String name;
private final List<Employee> employees = new ArrayList<>();
public Company(String name) {
this.name = name;
}
public String getName() {
return name;
}
public List<Employee> getEmployees() {
return employees;
}
}
public static void main(String[] args) {
Company company = new Company("Acme Corp");
company.getEmployees().add(new Employee("Alice", 28, new Address("Beijing")));
company.getEmployees().add(new Employee("Bob", 35, new Address("Shanghai")));
JXPathContext ctx = JXPathContext.newContext(company);
System.out.println("name = " + ctx.getValue("name"));
System.out.println("employees[1]/name = " + ctx.getValue("employees[1]/name"));
System.out.println("employees[age > 30]/name = " + ctx.getValue("employees[age > 30]/name"));
System.out.println("employees/address/city = " + ctx.getValue("employees/address/city"));
}
}
用主 jar 编译运行:
cd /opt/jxpath-dist-demo && javac -cp /opt/jxpath-dist/commons-jxpath-1.4.0.jar Main.java && echo JAVAC_OK && java -cp .:/opt/jxpath-dist/commons-jxpath-1.4.0.jar Main 2>&1
输出:
JAVAC_OK
name = Acme Corp
employees[1]/name = Alice
employees[age > 30]/name = Bob
employees/address/city = Beijing
JAVAC_OK 与 4 行正确输出确认:主 jar 单文件即可编译运行,结果与 Maven 方式完全一致。完整安装流程见《Ubuntu 22.04 安装 Apache Commons JXPath 指南》第五章。
八、集成到业务工程
按宿主工程的构建形态选两种集成方式之一,两者运行期行为完全一致。
方式 1:Maven 依赖(推荐)。依赖声明一行即可,无传递依赖:
<dependency>
<groupId>commons-jxpath</groupId>
<artifactId>commons-jxpath</artifactId>
<version>1.4.0</version>
</dependency>
两点注意:坐标必须用旧 groupId commons-jxpath(org.apache.commons 下 404);1.4.0 将 org.jdom:jdom:1.1.3 与 commons-beanutils:commons-beanutils:1.10.1 声明为 optional,默认的 Bean/DOM 导航不需要它们,仅在使用 JDOM 文档模型等扩展特性时再显式引入(servlet-api/jsp-api 为 provided+optional,仅 Web 层场景需要且由 Servlet 运行环境提供)。
方式 2:纯 jar 分发(无构建工具或内网直装)。把 commons-jxpath-1.4.0.jar 随应用一起部署,编译运行时挂 classpath 即可。运行期 classpath 的实测构成(第三章 CP= 回显)只有这一个 jar:
CP=/root/.m2/repository/commons-jxpath/commons-jxpath/1.4.0/commons-jxpath-1.4.0.jar
即集成最小集为「应用 jar + commons-jxpath-1.4.0.jar」两个文件,无需携带 commons-logging 等任何第三方 jar(第五章全程无 NoClassDefFoundError 可证)。需要 JXPath 官方源码/文档查阅时,用官方二进制包里附带的 sources.jar、javadoc.jar 或 apidocs/ 目录。
九、注意事项
- 点式写法必报错:
employees[1].name抛JXPathInvalidSyntaxException,必须写employees[1]/name;从 BeanUtils 点式路径迁移时逐一改写。 - 按路径创建必须先
setFactory:createPathAndSetValue遇缺失路径抛JXPathException: Factory is not set on the JXPathContext;单纯的setValue不需要工厂。 - XML 上
selectNodes返回 Element:类型为 JDK 内置DeferredElementNSImpl,取文本用getTextContent();Bean 上下文返回值本身。 - XML 相对上下文用
getNode():pointer.getValue()对元素返回文本聚合值(如Clean CodeRobert C. Martin32.50),据此建子上下文会抛JXPathNotFoundException。 - JDK 版本一致性:用哪个 JDK 编译就用哪个运行;多 JDK 并存系统上 PATH 的
java可能低于编译版本而抛UnsupportedClassVersionError(class file 61.0 对 52.0 的实测案例见《CentOS 8 安装 Apache Commons JXPath 指南》4.6 节)。 - 浮点语义:
sum()等 XPath 数值函数是 double 语义,实测sum(//book/price) = 117.49000000000001;金额精确计算取值后用BigDecimal。 - 版本坐标:只有
commons-jxpath:commons-jxpath:1.4.0(2025 年恢复发行)可用;1.3 为 2008 年断代旧版,勿在新项目使用。
十、常见问题(FAQ)
Q1:为什么 employees[1].name 会抛异常?
JXPath 不支持点式续写,谓词或下标后必须用 /。实测:
getValue("employees[1].name") threw org.apache.commons.jxpath.JXPathInvalidSyntaxException
message: Invalid XPath: 'employees[1].name'. Syntax error after: 'employees[1]'
getValue("employees[1]/name") = Alice
Q2:createPathAndSetValue 报 Factory is not set 怎么办?
先 ctx.setFactory 注册 AbstractFactory 实现,在 createObject 里按父对象与属性名创建缺失子对象。实测对照:
without AbstractFactory threw org.apache.commons.jxpath.JXPathException
root cause: Factory is not set on the JXPathContext - cannot create path: /employees[3]/address
with AbstractFactory, Carol now -> Employee{name='Carol', age=41, address=Address{city='Shenzhen'}}
Q3:XML 上 selectNodes 拿到的对象 toString 是 [title: null],怎么取文字?
那是 DOM Element 的 toString。转型 org.w3c.dom.Element 后 getTextContent(),实测:
item class = com.sun.org.apache.xerces.internal.dom.DeferredElementNSImpl, toString = [title: null]
as Element, getTextContent() = Java Concurrency in Practice
Q4:报 No value for xpath: 'Clean CodeRobert C. Martin32.50'/title 怎么办?
建子上下文用了 pointer.getValue()(文本聚合值)。改用 pointer.getNode() 取原始 Element,实测:
via getValue() threw org.apache.commons.jxpath.JXPathNotFoundException
message: No value for xpath: 'Clean CodeRobert C. Martin32.50'/title
via getNode(): title = Clean Code
via getNode(): price/@currency = USD
Q5:运行时抛 UnsupportedClassVersionError 怎么办?
编译与运行用了不同 JDK 版本(如 17 编译、1.8 运行)。统一 JDK 版本或用编译版本的完整路径运行;CentOS 8 多 JDK 并存的完整排查过程见《CentOS 8 安装 Apache Commons JXPath 指南》第八章 Q1。
十一、参考链接
- Apache Commons JXPath 官方主页:https://commons.apache.org/proper/commons-jxpath/
- JXPath 用户指南(User Guide):https://commons.apache.org/proper/commons-jxpath/userguide.html
- JXPath JavaDoc API 文档:https://commons.apache.org/proper/commons-jxpath/apidocs/index.html
- 官方发行下载目录:https://downloads.apache.org/commons/jxpath/
- Maven 中央仓库坐标目录(旧 groupId):https://repo.maven.apache.org/maven2/commons-jxpath/commons-jxpath/