Apache XMLBeans 5.4.1 XmlObject 与 Schema API 详解
本文以 Apache XMLBeans 5.4.1 为对象,在 Ubuntu 22.04 + OpenJDK 17 实测环境下详解 XmlObject 通用解析、XmlCursor 游标导航与修改、QName 与 Schema 类型系统 API,全部代码与回显真实可复现。
Apache XMLBeans 5.4.1 XmlObject 与 Schema API 详解
目标:在 Ubuntu 22.04 + OpenJDK 17 环境下,实测并详解 Apache XMLBeans 5.4.1 的三大核心 API 面:XmlObject 通用解析与选取、XmlCursor 游标导航与修改、QName 与 Schema 类型系统自省,全部代码与回显真实可复现。
一、环境信息
| 项 | 值 |
|---|---|
| 操作系统 | Ubuntu 22.04.5 LTS(x86_64) |
| JDK | OpenJDK 17.0.20.1 |
| Maven | Apache Maven 3.6.3 |
| XMLBeans | org.apache.xmlbeans:xmlbeans:5.4.1 |
| 类型 jar | com.example:order-xmlbeans:1.0.0(scomp 编译 /work/schema/order.xsd 产物,含 types.xsd include) |
| 工程目录 | /work/mvn-quick(Saxon-HE 12.10 已在依赖中) |
工程与 Schema 沿用《Apache XMLBeans 5.4.1 快速入门与集成示例》的订单示例(http://example.com/order,映射为 Java 包 com.example.order),本文继续在该工程里新增两个演示类并实测。
二、API 总览与继承关系
先以 javap 实测 5.4.1 jar 的真实接口结构,避免凭旧资料记忆写代码:
# J=~/.m2/repository/org/apache/xmlbeans/xmlbeans/5.4.1/xmlbeans-5.4.1.jar
# javap -cp $J org.apache.xmlbeans.XmlObject | sed -n '2p'
public interface org.apache.xmlbeans.XmlObject extends org.apache.xmlbeans.XmlTokenSource {
# javap -cp $J org.apache.xmlbeans.XmlCursor | sed -n '2p'
public interface org.apache.xmlbeans.XmlCursor extends org.apache.xmlbeans.XmlTokenSource,java.lang.AutoCloseable {
# javap -cp $J org.apache.xmlbeans.SimpleValue | sed -n '2p'
public interface org.apache.xmlbeans.SimpleValue extends org.apache.xmlbeans.XmlObject {
继承关系与职责:
| 接口 | 继承 | 职责 |
|---|---|---|
XmlObject | extends XmlTokenSource | 万物起点:解析、按 QName 选取子元素/属性、校验、execQuery |
XmlTokenSource | —— | xmlText() 序列化、newCursor()、save(File/OutputStream/Writer) |
XmlCursor | extends XmlTokenSource, AutoCloseable | 底层游标:token 级导航、改名属性/文本、跨对象复制 |
SimpleValue | extends XmlObject | 简单类型的文本值读写(getStringValue() 在这里,5.x 不在 XmlObject 上) |
XmlOptions | —— | 解析/校验/序列化的参数载体(错误监听、缩进等) |
SchemaType/SchemaTypeLoader/SchemaGlobalElement/SchemaParticle | —— | Schema 元数据自省面 |
XmlTokenSource 上实测存在的方法(节选 javap 输出):
public abstract org.apache.xmlbeans.XmlCursor newCursor();
public abstract java.lang.String xmlText();
public abstract java.lang.String xmlText(org.apache.xmlbeans.XmlOptions);
public abstract void save(java.io.File) throws java.io.IOException;
public abstract void save(java.io.OutputStream) throws java.io.IOException;
💡 技术拓展:XmlBeans 的双层设计——XmlObject 是"对象视图"(类型化/半类型化访问),XmlCursor 是"令牌视图"(START/ATTR/TEXT/END 令牌流)。对象视图易用但受 Schema 编译产物约束,令牌视图可做任意结构性修改(如本文第五节的 copyXml 追加)。需要细粒度操作时两者可以互相转换:
XmlObject.newCursor()与XmlCursor.getObject()。
三、示例工程与演示源码
工程 /work/mvn-quick 的 pom 与 Schema 见快速入门篇;本文新增 CursorDemo.java 与 QNameSchemaDemo.java(GenericDemo.java 见第三节)。order-xmlbeans.jar 需先装入本地仓库(命令见快速入门篇 4.2)。
/work/mvn-quick/src/main/java/demo/CursorDemo.java:
package demo;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlObject;
import javax.xml.namespace.QName;
import java.io.File;
public class CursorDemo {
static final String NS = "http://example.com/order";
static final QName Q_ORDER = new QName(NS, "order");
static final QName Q_ITEM = new QName(NS, "item");
static final QName Q_PRICE = new QName(NS, "price");
static final QName Q_NAME = new QName(NS, "name");
static final QName Q_ID = new QName("", "id");
static final QName Q_SKU = new QName("", "sku");
public static void main(String[] args) throws Exception {
XmlObject doc = XmlObject.Factory.parse(new File("/work/schema/order.xml"));
try (XmlCursor cursor = doc.newCursor()) {
cursor.toStartDoc();
cursor.toFirstChild();
System.out.println("[1] toFirstChild -> " + cursor.getName()
+ " id 属性 = " + cursor.getAttributeText(Q_ID));
cursor.setAttributeText(Q_ID, "B-2026-1101");
System.out.println("[2] setAttributeText 后 id = " + cursor.getAttributeText(Q_ID));
cursor.toChild(new QName(NS, "customer"));
cursor.toChild(Q_NAME);
System.out.println("[3] customer/name = " + cursor.getTextValue());
cursor.toStartDoc();
cursor.toFirstChild();
cursor.toChild(Q_ITEM);
String sku1 = cursor.getAttributeText(Q_SKU);
cursor.toChild(Q_NAME);
System.out.println("[4] 第 1 个 item sku = " + sku1
+ " name = " + cursor.getTextValue());
cursor.toStartDoc();
cursor.toFirstChild();
cursor.toChild(Q_ITEM);
cursor.toNextSibling();
cursor.toNextSibling();
System.out.println("[5] toNextSibling×2 后 sku = " + cursor.getAttributeText(Q_SKU));
cursor.toChild(Q_PRICE);
cursor.setTextValue("259.9");
System.out.println("[6] 第 3 个 item price 已改为 " + cursor.getTextValue());
cursor.toStartDoc();
cursor.toFirstChild();
cursor.toEndToken();
System.out.println("[7] 游标已定位到 order 的 END token,准备追加新 item");
}
XmlObject fragment = XmlObject.Factory.parse(
"<ord:item xmlns:ord='" + NS + "' sku='SKU-4004' qty='1'>"
+ "<ord:name>无线鼠标</ord:name><ord:price>199.0</ord:price></ord:item>");
try (XmlCursor target = doc.newCursor(); XmlCursor src = fragment.newCursor()) {
target.toStartDoc();
target.toFirstChild();
target.toEndToken();
src.toStartDoc();
src.toFirstChild();
System.out.println("[8] copyXml 返回 = " + src.copyXml(target));
}
System.out.println("[9] 追加后 item 数量 = "
+ doc.selectChildren(Q_ORDER)[0].selectChildren(Q_ITEM).length);
doc.save(new File("/work/schema/order-cursor-modified.xml"));
System.out.println("[10] 已保存 /work/schema/order-cursor-modified.xml");
}
}
/work/mvn-quick/src/main/java/demo/QNameSchemaDemo.java:
package demo;
import com.example.order.OrderDocument;
import org.apache.xmlbeans.SchemaGlobalElement;
import org.apache.xmlbeans.SchemaParticle;
import org.apache.xmlbeans.SchemaProperty;
import org.apache.xmlbeans.SchemaType;
import org.apache.xmlbeans.XmlBeans;
import org.apache.xmlbeans.XmlObject;
import javax.xml.namespace.QName;
import java.io.File;
public class QNameSchemaDemo {
static final String NS = "http://example.com/order";
public static void main(String[] args) throws Exception {
QName Q_ORDER = new QName(NS, "order");
QName Q_ITEM = new QName(NS, "item");
QName Q_ID = new QName("", "id");
System.out.println("[1] QName 表示 = " + Q_ORDER
+ " namespaceURI = " + Q_ORDER.getNamespaceURI()
+ " localPart = " + Q_ORDER.getLocalPart());
OrderDocument doc = OrderDocument.Factory.parse(new File("/work/schema/order.xml"));
XmlObject order = doc.selectChildren(Q_ORDER)[0];
System.out.println("[2] selectChildren(order) 命中,id = "
+ ((org.apache.xmlbeans.SimpleValue) order.selectAttribute(Q_ID)).getStringValue());
System.out.println("[3] selectChildren(item) 命中 = "
+ order.selectChildren(Q_ITEM).length);
SchemaType docType = doc.schemaType();
System.out.println("[4] 文档类型 isDocumentType = " + docType.isDocumentType()
+ " getName = " + docType.getName());
SchemaGlobalElement ge = XmlBeans.getContextTypeLoader().findElement(Q_ORDER);
System.out.println("[5] findElement(order).getName = " + ge.getName());
SchemaType orderType = ge.getType();
System.out.println("[6] OrderType = " + orderType.getName()
+ " isSimpleType = " + orderType.isSimpleType()
+ " baseType = " + orderType.getBaseType().getName());
System.out.println("[7] OrderType 元素属性:");
for (SchemaProperty p : orderType.getElementProperties()) {
System.out.println(" " + p.getName().getLocalPart()
+ " -> " + p.getType().getName());
}
System.out.println("[8] OrderType 属性型属性:");
for (SchemaProperty p : orderType.getAttributeProperties()) {
System.out.println(" @" + p.getName().getLocalPart()
+ " -> " + p.getType().getName());
}
SchemaParticle cm = orderType.getContentModel();
System.out.println("[9] contentModel particleType = " + cm.getParticleType()
+ "(SEQUENCE=" + SchemaParticle.SEQUENCE + ")"
+ " 子粒子数 = " + cm.countOfParticleChild());
SchemaParticle itemParticle = cm.getParticleChild(2);
System.out.println("[10] item 粒子 particleType = " + itemParticle.getParticleType()
+ "(ELEMENT=" + SchemaParticle.ELEMENT + ")"
+ " maxOccurs = " + itemParticle.getIntMaxOccurs());
}
}
编译:
# cd /work/mvn-quick && mvn -q -B compile
# echo $?
0
四、XmlObject:通用解析与选取
4.1 引导:无 XSD 也能解析
XmlObject.Factory.parse() 是通用入口,运行命令(类路径不含类型 jar 时):
# M2=~/.m2/repository
# java -Dfile.encoding=UTF-8 -cp target/classes:$M2/org/apache/xmlbeans/xmlbeans/5.4.1/xmlbeans-5.4.1.jar:$M2/org/apache/logging/log4j/log4j-api/2.26.1/log4j-api-2.26.1.jar demo.GenericDemo
解析完成,schemaType = N=
selectChildren(order) 命中 = 1
selectAttribute(id).getStringValue() = A-2026-1015
customer/email 文本值 = zhangwei@example.com
缩进序列化首行 = <?xml version="1.0" encoding="UTF-8"?>
schemaType = N= 的语义:类路径上没有该文档的 Schema 类型系统,解析树按"无类型"(NoType)处理,所有节点仍是合法的 XmlObject。把类型 jar 加上类路径后再跑同一段代码:
# java -Dfile.encoding=UTF-8 -cp target/classes:$M2/org/apache/xmlbeans/xmlbeans/5.4.1/xmlbeans-5.4.1.jar:$M2/org/apache/logging/log4j/log4j-api/2.26.1/log4j-api-2.26.1.jar:$M2/com/example/order-xmlbeans/1.0.0/order-xmlbeans-1.0.0.jar demo.GenericDemo
解析完成,schemaType = D=order@http://example.com/order
...
同一行变成 D=order@http://example.com/order——文档类型被自动绑定,本文第六节的 Schema 自省才有完整元数据。
4.2 按 QName 选取
实测在用的三组方法(均在 XmlObject 上):
| 方法 | 作用 | 实测结果 |
|---|---|---|
selectChildren(QName) | 取全部同名子元素 | doc.selectChildren(Q_ORDER) 命中 1;order.selectChildren(Q_ITEM) 命中 3 |
selectAttribute(QName) | 取指定属性 | order.selectAttribute(Q_ID) 取到 id 属性对象 |
schemaType() | 查看对象绑定类型 | N=(无类型)/ D=order@...(文档类型) |
注意两点:属性没有命名空间前缀时用 new QName("", "id")(空 namespaceURI);selectAttribute/selectChildren 找不到时返回空数组/空对象而不是异常,适合防御式读取。取文本值统一走 (SimpleValue) obj).getStringValue()(原因见第七节 API 差异)。
4.3 序列化
doc.xmlText(new XmlOptions().setSavePrettyPrint()) 输出 2 空格缩进、声明保留 UTF-8 的完整文档;doc.save(File) 直接落盘(实测见第五节 [10])。两者都继承自 XmlTokenSource,5.x 版本下这两个能力与旧版一致。
五、XmlCursor:导航、修改与复制
5.1 运行实测
# M2=~/.m2/repository
# java -Dfile.encoding=UTF-8 -cp target/classes:$M2/org/apache/xmlbeans/xmlbeans/5.4.1/xmlbeans-5.4.1.jar:$M2/org/apache/logging/log4j/log4j-api/2.26.1/log4j-api-2.26.1.jar:$M2/com/example/order-xmlbeans/1.0.0/order-xmlbeans-1.0.0.jar demo.CursorDemo
[1] toFirstChild -> {http://example.com/order}order id 属性 = A-2026-1015
[2] setAttributeText 后 id = B-2026-1101
[3] customer/name = 张伟
[4] 第 1 个 item sku = SKU-1001 name = 机械键盘
[5] toNextSibling×2 后 sku = SKU-3003
[6] 第 3 个 item price 已改为 259.9
[7] 游标已定位到 order 的 END token,准备追加新 item
[8] copyXml 返回 = true
[9] 追加后 item 数量 = 4
[10] 已保存 /work/schema/order-cursor-modified.xml
5.2 步骤解读
| 步骤 | API | 说明 |
|---|---|---|
| [1] | toStartDoc() → toFirstChild() → getName()/getAttributeText(QName) | 游标初始在 STARTDOC 令牌;getName() 返回元素的完整 QName |
| [2] | setAttributeText(QName, String) | 原地改属性值,返回 boolean 表示是否命中 |
| [3] | toChild(QName) + getTextValue() | 逐层下钻到叶子元素取文本 |
| [4][5] | toChild() → toNextSibling() | 同级遍历:第 1 个 item → 第 3 个 item |
| [6] | setTextValue(String) | 改叶子元素文本 |
| [7] | toEndToken() | 定位到 order 元素的 END 令牌(最后一个子元素之后) |
| [8] | copyXml(XmlCursor) | 把另一个游标当前令牌(新 item 的 START..END)复制到目标位置,返回 true |
| [9][10] | selectChildren() 复查 + save(File) | item 从 3 变 4,修改结果落盘 |
验证产物(保存后的文件已含新 item 与改过的属性):
# grep -o 'sku="SKU-4004"' /work/schema/order-cursor-modified.xml
sku="SKU-4004"
# grep -o 'id="B-2026-1101"' /work/schema/order-cursor-modified.xml
id="B-2026-1101"
5.3 游标使用要点
XmlCursor实现了AutoCloseable(实测接口声明见第二节),用 try-with-resources 管理生命周期;旧的dispose()仍然存在但不必再用。- 游标移动失败不会抛异常而是返回 false(如
toChild找不到目标),链式导航时注意返回值。 - 在 END token 处
insertElementWithText/copyXml追加的节点落在"最后一个子元素之后",正符合本例在 items 尾部加记录的语义。 - 修改不立即回写文件,必须显式
save()。
六、QName 与 Schema 类型系统
6.1 运行实测
# M2=~/.m2/repository
# java -Dfile.encoding=UTF-8 -cp target/classes:$M2/org/apache/xmlbeans/xmlbeans/5.4.1/xmlbeans-5.4.1.jar:$M2/org/apache/logging/log4j/log4j-api/2.26.1/log4j-api-2.26.1.jar:$M2/com/example/order-xmlbeans/1.0.0/order-xmlbeans-1.0.0.jar demo.QNameSchemaDemo
[1] QName 表示 = {http://example.com/order}order namespaceURI = http://example.com/order localPart = order
[2] selectChildren(order) 命中,id = A-2026-1015
[3] selectChildren(item) 命中 = 3
[4] 文档类型 isDocumentType = true getName = null
[5] findElement(order).getName = {http://example.com/order}order
[6] OrderType = {http://example.com/order}OrderType isSimpleType = false baseType = {http://www.w3.org/2001/XMLSchema}anyType
[7] OrderType 元素属性:
customer -> {http://example.com/order}CustomerType
shippingAddress -> {http://example.com/order}AddressType
item -> {http://example.com/order}ItemType
[8] OrderType 属性型属性:
@id -> {http://www.w3.org/2001/XMLSchema}string
@date -> {http://www.w3.org/2001/XMLSchema}date
[9] contentModel particleType = 3(SEQUENCE=3) 子粒子数 = 3
[10] item 粒子 particleType = 4(ELEMENT=4) maxOccurs = 2147483647
6.2 结果解读
- QName 字符串化:
{namespaceURI}localPart是 XMLBeans 全线的类型/元素命名格式,日志里出现{http://example.com/order}OrderType即此格式。 - 文档类型
getName() = null:文档类型(包裹全局元素的合成类型)没有 Schema 名称,isDocumentType() = true才是它的身份判据;真正带名字的是它包裹的全局元素order([5] 的findElement结果)。 - 类型继承:
OrderType的 baseType 是anyType([6]),因为示例 XSD 未声明扩展/限制基类。 - 属性自省:
getElementProperties()/getAttributeProperties()返回SchemaProperty列表,[7][8] 与 XSD 声明一一对应;@id/@date类型分别是 string/date。 - 内容模型:
getContentModel()返回SchemaParticle;实测 OrderType 顶层是 SEQUENCE(粒子类型常量 3),含 3 个子粒子;第 3 个子粒子 item 是 ELEMENT(常量 4),getIntMaxOccurs()返回 2147483647,即maxOccurs="unbounded"的内部表示。 findElement(QName):挂在SchemaTypeLoader(XmlBeans.getContextTypeLoader())上而非 XmlObject 上;能查到的前提是类型 jar 在类路径且对应类已加载(本例OrderDocument.Factory.parse触发加载)。
6.3 QName 实用速查
| 场景 | 写法 |
|---|---|
| 带命名空间元素 | new QName("http://example.com/order", "order") |
| 无命名空间属性 | new QName("", "id") |
| 元素选取 | xmlObject.selectChildren(qname) |
| 属性读取 | cursor.getAttributeText(qname) / xmlObject.selectAttribute(qname) |
| 游标下钻 | cursor.toChild(qname) |
| 类型系统查找 | XmlBeans.getContextTypeLoader().findElement(qname) |
七、5.x API 差异与迁移要点
旧教程(2.x/3.x 时代)与 5.4.1 的实测差异汇总,全部以 javap 与编译结果为准:
| 旧写法 | 5.4.1 现状 | 实测证据 |
|---|---|---|
xmlObject.getStringValue() | XmlObject 上不存在,改 (SimpleValue) obj).getStringValue() | javap XmlObject 无该方法;javac 报 cannot find symbol |
new XmlOptions().setSavePretty() | 方法名改为 setSavePrettyPrint() | `javap XmlOptions |
全局元素查找 findElement | 挂在 SchemaTypeLoader,不在 XmlObject | javap SchemaTypeLoader 含 findElement(javax.xml.namespace.QName) |
cursor.dispose() 释放 | 改为 close()(AutoCloseable),try-with-resources | javap XmlCursor 接口声明含 AutoCloseable |
| 无类型/有类型解析 | schemaType() 显示 N=(无)或 D=order@ns(有) | 两套类路径下的运行回显见 4.1 |
八、注意事项
- 类型 jar 决定自省深度:第六节所有 Schema 元数据接口都依赖
order-xmlbeans.jar在类路径;移除后findElement查不到全局元素、schemaType()退回N=。发布应用时它是运行期必需品,不是编译期可选件。 - QName 的 namespaceURI 别写错大小写与斜杠:XMLBeans 按字面匹配,
http://example.com/order/(多了斜杠)会静默查不到。 - 修改后必须 save():游标与 XmlObject 上的全部修改只存在于内存对象,第五节 [10] 的落盘动作不能省。
- 中文编码:本环境默认平台编码是 ANSI_X3.4-1968,运行参数必须带
-Dfile.encoding=UTF-8,源码用javac -encoding UTF-8,否则输出中文变问号(安装指南 FAQ 有复现与修复)。
九、常见问题(FAQ)
Q1:selectChildren(Q_NAME) 返回空数组但 XML 里明明有?
检查 QName 的 namespaceURI:文档根声明了 xmlns:ord="http://example.com/order" 且 elementFormDefault="qualified",子元素全部带命名空间,必须用 new QName(NS, "item");只有无前缀属性(如 id/sku)才用空 URI。
Q2:(SimpleValue) obj 强转会 ClassCastException 吗?
元素/属性对象都实现 SimpleValue,本示例所有取值路径实测未出现该异常;对纯复合元素(如 customer)取文本值没有意义,应继续 selectChildren 下钻。
Q3:游标移动后位置和预期不符?
toChild(Q_NAME) 等方法是"相对当前位置"移动且返回 boolean;示例 [4] 特意用变量先取 sku 再下钻,避免在同一表达式里混用"移动 + 取值"的副作用顺序。
Q4:文档类型 getName() 为什么是 null?
见 6.2:文档类型是包裹全局元素的合成类型,本身无 Schema 名;判别用 isDocumentType(),取全局元素名用 findElement(QName).getName()。
十、参考链接
- XmlObject Javadoc(5.4.1):https://xmlbeans.apache.org/apidocs/5.4.1/org/apache/xmlbeans/XmlObject.html
- XmlCursor Javadoc(5.4.1):https://xmlbeans.apache.org/apidocs/5.4.1/org/apache/xmlbeans/XmlCursor.html
- SchemaType Javadoc(5.4.1):https://xmlbeans.apache.org/apidocs/5.4.1/org/apache/xmlbeans/SchemaType.html
- 本系列姊妹篇:《Apache XMLBeans 5.4.1 快速入门与集成示例》《Apache XMLBeans 5.4.1 scomp 编译与进阶实战详解》