Apache Axiom 1.4.0 构建并运行示例应用指南

Apache Axiom 1.4.0 构建并运行示例应用指南:从零创建 Maven 工程,实测五大示例——OMFactory 建树序列化、InputStream 解析与延迟解析验证、遍历修改删除、axiom-impl/axiom-dom 双实现切换与 w3c DOM 互操作、XPath 查询,含 mvn exec:java 与 java -cp 两种运行方式。

Apache AxiomAxiom2026/10/13

Apache Axiom 1.4.0 构建并运行示例应用指南

目标:在 Ubuntu 22.04 上从零创建一个 Maven 工程,构建并运行覆盖 Axiom 1.4.0 核心能力的五个示例应用:OMFactory 建树与序列化、从 InputStream 解析(延迟解析实测)、OMElement 遍历与修改、axiom-impl 与 axiom-dom 双实现切换(含 w3c DOM 互操作)、XPath 查询。每个示例给出完整源码、编译运行命令与实测回显。所有命令与输出均在 Ubuntu 22.04.5 + OpenJDK 17 + Maven 3.6.3 环境实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS(jammy)
  • 架构:x86_64
  • Java:OpenJDK 17.0.20.1(Ubuntu 官方源 openjdk-17-jdk-headless)
  • 构建工具:Apache Maven 3.6.3
  • Axiom 版本:1.4.0(axiom-api 1.4.0 编译期 + axiom-impl 1.4.0 / axiom-dom 1.4.0 运行期)
  • 示例工程目录:/root/axiom-demo/
  • 演示账号:root

💡 说明:Axiom 是纯 Java 库,「示例应用」即调用其 jar 的普通 Java 程序。本文所有示例只需 JDK + Maven + 三个 Axiom 制品即可运行,不涉及任何服务进程。

二、前置条件

2.1 安装 JDK 与 Maven

apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y openjdk-17-jdk-headless maven
java -version

实测回显:

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)

2.2 创建工程骨架与 pom.xml

mkdir -p /root/axiom-demo/src/main/java/demo
cd /root/axiom-demo

pom.xml 完整内容(UTF-8 编码必须显式声明,否则含中文注释的源文件编译报 unmappable character):

<?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>axiom-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <axiom.version>1.4.0</axiom.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.ws.commons.axiom</groupId>
            <artifactId>axiom-api</artifactId>
            <version>${axiom.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.ws.commons.axiom</groupId>
            <artifactId>axiom-impl</artifactId>
            <version>${axiom.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.ws.commons.axiom</groupId>
            <artifactId>axiom-dom</artifactId>
            <version>${axiom.version}</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</project>

说明:axiom-api 提供 OMFactory、OMElement、OMXMLBuilderFactory 等全部接口(编译期需要);axiom-impl 是默认 LLOM 拉式实现;axiom-dom 是可强转 w3c DOM 的包装实现;两者同为 runtime,可并存,运行期按 feature 选择。

验证依赖可解析并输出 classpath 文件(后续 java -cp 运行方式要用):

mvn -q dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt
wc -c /tmp/cp.txt

实测回显:

964 /tmp/cp.txt

三、示例一:OMFactory 建树与序列化(BuildTree)

本示例演示 Axiom 最基础的能力:用 OMFactory 程序化构造一棵带命名空间的 XML 树,再序列化输出。这是理解 Axiom 对象模型的起点——所有节点(元素/属性/命名空间/文本)都由同一个工厂创建。

写入 src/main/java/demo/BuildTree.java:

package demo;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;

import java.io.StringWriter;

public class BuildTree {
    public static void main(String[] args) throws Exception {
        OMFactory factory = OMAbstractFactory.getOMFactory();
        OMNamespace ns = factory.createOMNamespace("http://demo.example.com/book", "b");
        OMElement root = factory.createOMElement("bookstore", ns);
        OMElement book = factory.createOMElement("book", ns, root);
        book.addAttribute("category", "programming", null);
        OMElement title = factory.createOMElement("title", ns, book);
        title.setText("Axiom in Action");
        OMElement price = factory.createOMElement("price", ns, book);
        price.setText("39.90");

        StringWriter out = new StringWriter();
        root.serialize(out);
        System.out.println(out);
    }
}

代码要点:createOMElement(局部名, 命名空间, 父元素) 三参重载自动挂树;addAttribute(名, 值, null) 表示无命名空间属性;serialize(Writer) 输出整棵树。

编译并运行(先编译再执行):

mvn -q compile
mvn -q compile exec:java -Dexec.mainClass=demo.BuildTree

实测回显:

<b:bookstore xmlns:b="http://demo.example.com/book"><b:book category="programming"><b:title>Axiom in Action</b:title><b:price>39.90</b:price></b:book></b:bookstore>

验证手段:输出是命名空间前缀 b 正确声明、层次完整、属性就位的 XML,与代码构造的结构一一对应,即建树与序列化能力正常。

四、示例二:从 InputStream 解析与延迟解析验证(ParseDeferred)

本示例演示 Axiom 的招牌特性 deferred parsing(延迟解析):从 InputStream 构建 OM 树后,只访问根元素名和第一个子元素,底层输入流不会被读完;直到显式 build() 才把整棵树解析出来。用一个统计读取字节数的包装流来直观验证。

写入 src/main/java/demo/ParseDeferred.java:

package demo;

import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMXMLBuilderFactory;
import org.apache.axiom.om.OMXMLParserWrapper;

import javax.xml.namespace.QName;
import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

public class ParseDeferred {
    static class CountingInputStream extends FilterInputStream {
        long count = 0;
        CountingInputStream(InputStream in) { super(in); }
        public int read() throws IOException { int r = super.read(); if (r != -1) count++; return r; }
        public int read(byte[] b, int off, int len) throws IOException {
            int r = super.read(b, off, len);
            if (r != -1) count += r;
            return r;
        }
    }

    public static void main(String[] args) throws Exception {
        StringBuilder sb = new StringBuilder();
        sb.append("<catalog>");
        for (int i = 0; i < 5000; i++) {
            sb.append("<book id=\"").append(i).append("\"><title>Book ").append(i)
              .append("</title><price>").append(10 + i % 50).append("</price></book>");
        }
        sb.append("</catalog>");
        byte[] data = sb.toString().getBytes(StandardCharsets.UTF_8);
        System.out.println("document bytes = " + data.length);

        CountingInputStream in = new CountingInputStream(new ByteArrayInputStream(data));
        OMXMLParserWrapper builder = OMXMLBuilderFactory.createOMBuilder(in);
        OMElement root = builder.getDocumentElement();
        System.out.println("after getDocumentElement(): root=" + root.getLocalName()
                + ", bytes read=" + in.count + ", isCompleted=" + builder.isCompleted());
        OMElement first = root.getFirstElement();
        System.out.println("after getFirstElement(): first book id=" + first.getAttributeValue(new QName("id"))
                + ", bytes read=" + in.count);
        root.build();
        System.out.println("after root.build(): bytes read=" + in.count + " (all bytes read)");
        int n = 0;
        java.util.Iterator<OMElement> it = root.getChildElements();
        while (it.hasNext()) { it.next(); n++; }
        System.out.println("book element count = " + n + ", isCompleted=" + builder.isCompleted());
    }
}

编译并运行:

mvn -q compile
mvn -q compile exec:java -Dexec.mainClass=demo.ParseDeferred

实测回显:

document bytes = 317799
after getDocumentElement(): root=catalog, bytes read=4000, isCompleted=false
after getFirstElement(): first book id=0, bytes read=4000
after root.build(): bytes read=317799 (all bytes read)
book element count = 5000, isCompleted=false

结果解读与验证:

  • 31.8 万字节的文档,仅取根名 + 首个子元素时底层流只被读了 4000 字节(解析器缓冲区粒度),isCompleted=false——其余 49 个 book 的子树从未解析、从未占用内存,这就是延迟解析生效的直接证据;
  • root.build() 后流被读完(317799 字节),整棵树可全量遍历(count=5000);
  • isCompleted() 表示解析器是否已消费到文档末尾事件,即便业务上遍历完所有子元素它也可能保持 false,判断「树是否已全量构建」应以是否执行过 build() 为准。

五、示例三:OMElement 遍历、修改与删除(TraverseModify)

本示例演示树建成后的常见操作:按名遍历子元素、改文本、加属性、detach() 删除节点、递归走全树。写入 src/main/java/demo/TraverseModify.java:

package demo;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.om.OMNode;

import javax.xml.namespace.QName;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class TraverseModify {
    public static void main(String[] args) throws Exception {
        OMFactory f = OMAbstractFactory.getOMFactory();
        OMNamespace ns = f.createOMNamespace("http://demo.example.com/book", "b");
        QName BOOK = new QName(ns.getNamespaceURI(), "book");
        QName TITLE = new QName(ns.getNamespaceURI(), "title");
        QName PRICE = new QName(ns.getNamespaceURI(), "price");

        OMElement root = f.createOMElement("bookstore", ns);
        String[] titles = {"Axiom Basics", "XML Deep Dive", "StAX in Practice"};
        double[] prices = {39.90, 59.00, 45.50};
        for (int i = 0; i < titles.length; i++) {
            OMElement book = f.createOMElement("book", ns, root);
            book.addAttribute("category", "tech", null);
            f.createOMElement("title", ns, book).setText(titles[i]);
            f.createOMElement("price", ns, book).setText(String.valueOf(prices[i]));
        }

        System.out.println("-- 1. iterate --");
        Iterator<OMElement> it = root.getChildElements();
        while (it.hasNext()) {
            OMElement book = it.next();
            System.out.println("title=" + book.getFirstChildWithName(TITLE).getText()
                    + " price=" + book.getFirstChildWithName(PRICE).getText());
        }

        System.out.println("-- 2. modify: update price, add discount attr on 2nd book --");
        List<OMElement> books = new ArrayList<>();
        it = root.getChildElements();
        while (it.hasNext()) books.add(it.next());
        OMElement second = books.get(1);
        second.getFirstChildWithName(PRICE).setText("49.00");
        second.addAttribute("discount", "0.8", null);

        System.out.println("-- 3. remove: detach 3rd book --");
        books.get(2).detach();

        StringWriter sw = new StringWriter();
        root.serialize(sw);
        System.out.println("after modify: " + sw);

        System.out.println("-- 4. recursive walk --");
        int[] stats = new int[2];
        walk(root, stats);
        System.out.println("visited elements=" + stats[0] + ", all nodes=" + stats[1]);
    }

    static void walk(OMNode node, int[] stats) {
        stats[1]++;
        if (node instanceof OMElement) {
            stats[0]++;
            OMElement e = (OMElement) node;
            if (e.getFirstElement() == null && e.getText().trim().isEmpty()) {
                System.out.println("leaf: " + e.getLocalName()
                        + (e.getAttributeValue(new QName("category")) != null
                           ? " category=" + e.getAttributeValue(new QName("category")) : ""));
            }
        }
        if (node instanceof OMElement) {
            Iterator<OMNode> children = ((OMElement) node).getChildren();
            while (children.hasNext()) walk(children.next(), stats);
        }
    }
}

编译并运行:

mvn -q compile
mvn -q compile exec:java -Dexec.mainClass=demo.TraverseModify

实测回显:

-- 1. iterate --
title=Axiom Basics price=39.9
title=XML Deep Dive price=59.0
title=StAX in Practice price=45.5
-- 2. modify: update price, add discount attr on 2nd book --
-- 3. remove: detach 3rd book --
after modify: <b:bookstore xmlns:b="http://demo.example.com/book"><b:book category="tech"><b:title>Axiom Basics</b:title><b:price>39.9</b:price></b:book><b:book category="tech" discount="0.8"><b:title>XML Deep Dive</b:title><b:price>49.00</b:price></b:book></b:bookstore>
-- 4. recursive walk --
visited elements=7, all nodes=11

结果解读与验证:

  • 第 2 步把第二本书价格从 59.0 改为 49.00 并追加 discount="0.8" 属性,第 3 步 detach() 删除第三本书——最终序列化输出只剩两本书、第二本带 discount 属性且价格为 49.00,修改与删除均生效;
  • 递归走树统计到 7 个元素、11 个节点(11 = 7 元素 + 4 文本节点,删除一本后 3 本书的 title/price 文本),节点模型符合「元素 + 文本」两层结构;
  • 注意 setText(String.valueOf(59.00)) 输出为 59.0——文本按字符串原样保存,示例里用 double 转串导致尾部 0 丢失,生产代码直接传字符串可避免。

六、示例四:axiom-impl 与 axiom-dom 双实现切换(MetaFactoryDemo)

本示例演示 Axiom「一套 API、两种实现」的切换机制:默认走 axiom-impl(LLOM 拉式实现),通过 OMAbstractFactory.getMetaFactory(OMAbstractFactory.FEATURE_DOM) 切到 axiom-dom 实现——后者的节点实现了 org.w3c.dom.Element 接口,可直接传给任何接受 w3c DOM 的组件。写入 src/main/java/demo/MetaFactoryDemo.java:

package demo;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMMetaFactory;
import org.apache.axiom.om.OMXMLBuilderFactory;
import org.apache.axiom.om.OMXMLParserWrapper;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;

public class MetaFactoryDemo {
    public static void main(String[] args) throws Exception {
        String xml = "<msg xmlns=\"http://demo.example.com\"><body>hello axiom</body></msg>";

        System.out.println("META_FACTORY_NAME_PROPERTY = " + OMAbstractFactory.META_FACTORY_NAME_PROPERTY);

        OMFactory llFactory = OMAbstractFactory.getOMFactory();
        System.out.println("default OMFactory class = " + llFactory.getClass().getName());

        OMMetaFactory domMeta = OMAbstractFactory.getMetaFactory(OMAbstractFactory.FEATURE_DOM);
        OMFactory domFactory = domMeta.getOMFactory();
        System.out.println("dom OMFactory class    = " + domFactory.getClass().getName());

        OMXMLParserWrapper llBuilder = OMXMLBuilderFactory.createOMBuilder(
                new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
        OMElement llRoot = llBuilder.getDocumentElement();
        System.out.println("llom root node class = " + llRoot.getClass().getName()
                + ", text=" + llRoot.getFirstElement().getText());

        OMXMLParserWrapper domBuilder = OMXMLBuilderFactory.createOMBuilder(domFactory,
                new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
        OMElement domRoot = domBuilder.getDocumentElement();
        System.out.println("dom root node class  = " + domRoot.getClass().getName()
                + ", text=" + domRoot.getFirstElement().getText());

        try {
            org.w3c.dom.Element w3c = (org.w3c.dom.Element) domRoot;
            System.out.println("cast to w3c.dom.Element OK: tagName=" + w3c.getTagName()
                    + ", body text=" + w3c.getElementsByTagName("body").item(0).getTextContent());
        } catch (ClassCastException e) {
            System.out.println("cast to w3c.dom.Element FAILED: " + e);
        }
        try {
            org.w3c.dom.Element w3c2 = (org.w3c.dom.Element) llRoot;
            System.out.println("llom root cast to w3c.dom.Element: " + (w3c2 != null));
        } catch (ClassCastException e) {
            System.out.println("llom root cast to w3c.dom.Element FAILED (expected, LLOM is not DOM)");
        }
    }
}

编译并运行:

mvn -q compile
mvn -q compile exec:java -Dexec.mainClass=demo.MetaFactoryDemo

实测回显:

META_FACTORY_NAME_PROPERTY = org.apache.axiom.om.OMMetaFactory
default OMFactory class = org.apache.axiom.om.impl.common.factory.OMFactoryImpl
dom OMFactory class    = org.apache.axiom.om.impl.common.factory.OMFactoryImpl
llom root node class = org.apache.axiom.om.impl.llom.AxiomElementImpl, text=hello axiom
dom root node class  = org.apache.axiom.om.impl.dom.DOOMNSAwareElementImpl, text=hello axiom
cast to w3c.dom.Element OK: tagName=msg, body text=hello axiom
llom root cast to w3c.dom.Element FAILED (expected, LLOM is not DOM)

结果解读与验证:

  • 1.4.0 中两种实现共用同一个工厂类(org.apache.axiom.om.impl.common.factory.OMFactoryImpl,内部实现重构后的通用工厂),区分实现要看节点类:LLOM 是 om.impl.llom.AxiomElementImpl,DOM 是 om.impl.dom.DOOMNSAwareElementImpl;
  • axiom-dom 解析出的根节点可直接强转 org.w3c.dom.Element 并用 getElementsByTagName 等 w3c API 操作(回显 cast to w3c.dom.Element OK);LLOM 节点强转失败——需要与 w3c DOM 互操作时必须选 FEATURE_DOM 实现;
  • 切换实现的关键只有一处:给 OMXMLBuilderFactory.createOMBuilder() 传入哪个 OMFactory。

七、示例五:XPath 查询(XpathDemo,需 jaxen)

Axiom 自带 DocumentNavigator 把 OM 树适配给 Jaxen XPath 引擎(axiom-api 的传递依赖),可直接对 OM 树执行 XPath 查询。写入 src/main/java/demo/XpathDemo.java:

package demo;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.om.xpath.DocumentNavigator;
import org.jaxen.BaseXPath;
import org.jaxen.XPath;

import java.util.List;

public class XpathDemo {
    public static void main(String[] args) throws Exception {
        OMFactory f = OMAbstractFactory.getOMFactory();
        OMNamespace ns = f.createOMNamespace("http://demo.example.com/book", "b");
        OMElement root = f.createOMElement("bookstore", ns);
        String[] titles = {"Axiom Basics", "XML Deep Dive"};
        for (String t : titles) {
            OMElement book = f.createOMElement("book", ns, root);
            f.createOMElement("title", ns, book).setText(t);
        }
        BaseXPath xpath = new BaseXPath("//b:title", new DocumentNavigator());
        xpath.addNamespace("b", ns.getNamespaceURI());
        List<?> result = xpath.selectNodes(root);
        System.out.println("//b:title matched " + result.size() + " nodes");
        for (Object o : result) {
            System.out.println("  title text = " + ((OMElement) o).getText());
        }
    }
}

编译并运行:

mvn -q compile
mvn -q compile exec:java -Dexec.mainClass=demo.XpathDemo

实测回显:

//b:title matched 2 nodes
  title text = Axiom Basics
  title text = XML Deep Dive

验证手段://b:title 命中两个 title 元素并取出文本,与建树数据一致。两个易错点实测确认:org.jaxen.XPath 是接口不可直接 new(须用 BaseXPath);带前缀的 XPath 必须先 addNamespace("b", uri),否则报 UnresolvableException: Cannot resolve namespace prefix 'b'。

八、两种运行方式对比与打包

前文统一用 mvn compile exec:java 运行;不依赖 Maven 插件时,可用 dependency:build-classpath 生成的 classpath 直接 java -cp 运行(已在 2.2 节生成 /tmp/cp.txt):

mvn -q compile
java -cp "target/classes:$(cat /tmp/cp.txt)" demo.ParseDeferred

实测回显:

document bytes = 317799
after getDocumentElement(): root=catalog, bytes read=4000, isCompleted=false
after getFirstElement(): first book id=0, bytes read=4000
after root.build(): bytes read=317799 (all bytes read)
book element count = 5000, isCompleted=false
运行方式命令优点适用
mvn exec:javamvn compile exec:java -Dexec.mainClass=demo.X一条命令自动编译+运行开发调试
java -cpjava -cp "target/classes:$(cat /tmp/cp.txt)" demo.X不依赖插件、贴近生产启动形态脚本化、复现实验

生产打包:在 pom 中加 maven-shade-plugin 或 maven-dependency-plugin(copy-dependencies)把 runtime 依赖打进发布包,启动命令即 java -cp "app.jar:lib/*" 主类。因 Axiom 各 jar 属 OSGi bundle,也可直接部署进 OSGi 运行环境(Bundle-SymbolicName 已内置于 MANIFEST)。

九、常见问题(FAQ)

9.1 exec:java 报找不到主类或类未编译

现象:mvn exec:java -Dexec.mainClass=demo.X 报 ClassNotFoundException: demo.X。 解决:exec:java 默认不触发编译,先 mvn compile 再执行;或合并成 mvn compile exec:java -Dexec.mainClass=demo.X(本文示例均用此形态)。

9.2 程序里 System.out 输出中文变成 ? 号

现象:在未配置 UTF-8 locale 的终端里运行含中文输出的程序,控制台显示 ???。 解决:这是终端 stdout 编码问题而非 Axiom 问题。用 LANG=C.UTF-8 java -cp "target/classes:$(cat /tmp/cp.txt)" demo.X 启动,或 JVM 加 -Dfile.encoding=UTF-8 并确认终端本身为 UTF-8;XML 内容层面不受影响(序列化按 OMOutputFormat 的 charset 走)。

9.3 只引 axiom-api 编译能过,运行报 No meta factory found for feature 'default'

完整报错与解决见《Apache Axiom 1.4.0 安装指南》10.1 节;根因是运行期 classpath 缺 axiom-impl。本文示例工程同时引入 axiom-impl 与 axiom-dom,不存在此问题。

9.4 大文档解析吃内存怎么办

Axiom 默认延迟解析已大幅省内存(见示例二实测:31.8 万字节文档只访问根与首子元素时仅读 4000 字节)。进一步手段:只遍历不需要回改时用 root.getXMLStreamReaderWithoutCaching() 走纯流式(不建树);确实需要全树时再 build()。相关序列化/缓存参数详见《Apache Axiom 1.4.0 配置参数详解》。

十、参考链接