Apache Commons JXPath 1.4.0 快速入门与集成示例

JXPath 1.4.0 快速入门:Maven 依赖引入、Bean 对象图与 XML DOM 的 XPath 导航完整示例、官方二进制包免 Maven 验证与业务集成方式,代码完整可直接复制,输出全部实测。

Apache Commons JXPathApache Commons JXPath2026/10/14

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 JXPathXPath 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/ 目录。

九、注意事项

  1. 点式写法必报错:employees[1].name 抛 JXPathInvalidSyntaxException,必须写 employees[1]/name;从 BeanUtils 点式路径迁移时逐一改写。
  2. 按路径创建必须先 setFactory:createPathAndSetValue 遇缺失路径抛 JXPathException: Factory is not set on the JXPathContext;单纯的 setValue 不需要工厂。
  3. XML 上 selectNodes 返回 Element:类型为 JDK 内置 DeferredElementNSImpl,取文本用 getTextContent();Bean 上下文返回值本身。
  4. XML 相对上下文用 getNode():pointer.getValue() 对元素返回文本聚合值(如 Clean CodeRobert C. Martin32.50),据此建子上下文会抛 JXPathNotFoundException。
  5. JDK 版本一致性:用哪个 JDK 编译就用哪个运行;多 JDK 并存系统上 PATH 的 java 可能低于编译版本而抛 UnsupportedClassVersionError(class file 61.0 对 52.0 的实测案例见《CentOS 8 安装 Apache Commons JXPath 指南》4.6 节)。
  6. 浮点语义:sum() 等 XPath 数值函数是 double 语义,实测 sum(//book/price) = 117.49000000000001;金额精确计算取值后用 BigDecimal。
  7. 版本坐标:只有 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。

十一、参考链接

  1. Apache Commons JXPath 官方主页:https://commons.apache.org/proper/commons-jxpath/
  2. JXPath 用户指南(User Guide):https://commons.apache.org/proper/commons-jxpath/userguide.html
  3. JXPath JavaDoc API 文档:https://commons.apache.org/proper/commons-jxpath/apidocs/index.html
  4. 官方发行下载目录:https://downloads.apache.org/commons/jxpath/
  5. Maven 中央仓库坐标目录(旧 groupId):https://repo.maven.apache.org/maven2/commons-jxpath/commons-jxpath/