Apache XMLBeans 5.4.1 XmlObject 与 Schema API 详解

本文以 Apache XMLBeans 5.4.1 为对象,在 Ubuntu 22.04 + OpenJDK 17 实测环境下详解 XmlObject 通用解析、XmlCursor 游标导航与修改、QName 与 Schema 类型系统 API,全部代码与回显真实可复现。

Apache XMLBeansApache XMLBeans2026/10/15

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)
JDKOpenJDK 17.0.20.1
MavenApache Maven 3.6.3
XMLBeansorg.apache.xmlbeans:xmlbeans:5.4.1
类型 jarcom.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 {

继承关系与职责:

接口继承职责
XmlObjectextends XmlTokenSource万物起点:解析、按 QName 选取子元素/属性、校验、execQuery
XmlTokenSource——xmlText() 序列化、newCursor()、save(File/OutputStream/Writer)
XmlCursorextends XmlTokenSource, AutoCloseable底层游标:token 级导航、改名属性/文本、跨对象复制
SimpleValueextends 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 游标使用要点

  1. XmlCursor 实现了 AutoCloseable(实测接口声明见第二节),用 try-with-resources 管理生命周期;旧的 dispose() 仍然存在但不必再用。
  2. 游标移动失败不会抛异常而是返回 false(如 toChild 找不到目标),链式导航时注意返回值。
  3. 在 END token 处 insertElementWithText/copyXml 追加的节点落在"最后一个子元素之后",正符合本例在 items 尾部加记录的语义。
  4. 修改不立即回写文件,必须显式 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,不在 XmlObjectjavap SchemaTypeLoader 含 findElement(javax.xml.namespace.QName)
cursor.dispose() 释放改为 close()(AutoCloseable),try-with-resourcesjavap XmlCursor 接口声明含 AutoCloseable
无类型/有类型解析schemaType() 显示 N=(无)或 D=order@ns(有)两套类路径下的运行回显见 4.1

八、注意事项

  1. 类型 jar 决定自省深度:第六节所有 Schema 元数据接口都依赖 order-xmlbeans.jar 在类路径;移除后 findElement 查不到全局元素、schemaType() 退回 N=。发布应用时它是运行期必需品,不是编译期可选件。
  2. QName 的 namespaceURI 别写错大小写与斜杠:XMLBeans 按字面匹配,http://example.com/order/(多了斜杠)会静默查不到。
  3. 修改后必须 save():游标与 XmlObject 上的全部修改只存在于内存对象,第五节 [10] 的落盘动作不能省。
  4. 中文编码:本环境默认平台编码是 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()。

十、参考链接