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 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:java | mvn compile exec:java -Dexec.mainClass=demo.X | 一条命令自动编译+运行 | 开发调试 |
| java -cp | java -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 配置参数详解》。
十、参考链接
- Apache Axiom 项目主页:https://ws.apache.org/axiom/
- Maven Central axiom-api:https://repo1.maven.org/maven2/org/apache/ws/commons/axiom/axiom-api/
- Jaxen 项目主页:https://github.com/jaxen-xpath/jaxen
- 本批示例配套安装说明:《Apache Axiom 1.4.0 安装指南》