XSD 的「元素替换」(Substitution Group)是 XSD 中最优雅、最强大的多态机制之一,
它可以让一个元素“代替”另一个元素出现,非常像面向对象中的继承 + 多态。
核心概念(3 分钟彻底搞懂)
| 角色 | 英文 | 作用 | 必须满足的条件 |
|---|---|---|---|
| 头元素(Head) | head element | 被别人替换的“父元素” | 必须是全局元素(直接写在 xs:schema 下) |
| 成员元素(Member) | member element | 可以代替头元素出现的“子元素” | 必须用 substitutionGroup="头元素名" |
| 类型要求 | — | 成员元素的类型必须是头元素类型的派生类型(可以相同) | 包括 extension、restriction、甚至同一个类型 |
最经典的例子:XML 购货单(PO – Purchase Order)
<!-- 实际 XML 中可以这样写(三种都合法) -->
<shipTo>张三</shipTo> <!-- 原始 -->
<customerNumber>10086</customerNumber> <!-- 用子元素替换了 shipTo -->
<vipCustomer vipLevel="gold">李四</vipCustomer> <!-- 又一个替换 -->
对应的完整 XSD(背下来就无敌了):
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/po"
xmlns:po="http://example.com/po"
elementFormDefault="qualified">
<!-- 1. 头元素(必须是全局元素) -->
<xs:element name="shipTo" type="po:AddressType"/>
<!-- 2. 成员元素 1:用客户编号代替地址 -->
<xs:element name="customerNumber" type="xs:integer"
substitutionGroup="po:shipTo"/> <!-- 关键就是这一行 -->
<!-- 3. 成员元素 2:VIP客户,带额外属性 -->
<xs:element name="vipCustomer" substitutionGroup="po:shipTo">
<xs:complexType>
<xs:complexContent>
<xs:extension base="po:AddressType">
<xs:attribute name="vipLevel" type="xs:string"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:element>
<!-- 地址类型 -->
<xs:complexType name="AddressType">
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="street" type="xs:string"/>
<xs:element name="city" type="xs:string"/>
</xs:sequence>
</xs:complexType>
<!-- 订单根元素 -->
<xs:element name="purchaseOrder">
<xs:complexType>
<xs:sequence>
<!-- 这里只能写头元素,但实例中可以用任意成员替换它 -->
<xs:element ref="po:shipTo"/>
<xs:element name="items" type="po:ItemsType"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
运行结果:下面三段 XML 全部通过验证!
<!-- 写法1:用原始元素 -->
<po:purchaseOrder>
<po:shipTo>...</po:shipTo>
</po:purchaseOrder>
<!-- 写法2:用 customerNumber 完全代替地址 -->
<po:purchaseOrder>
<po:customerNumber>88888</po:customerNumber>
</po:purchaseOrder>
<!-- 写法3:用带 vipLevel 的 vipCustomer 代替 -->
<po:purchaseOrder>
<po:vipCustomer vipLevel="platinum">王五</po:vipCustomer>
</po:purchaseOrder>
常见使用场景(真实项目都在用)
| 场景 | 头元素 | 常见成员元素 | 典型项目 |
|---|---|---|---|
| XHTML | <h> | <h1> ~ <h6> | XHTML 1.0/1.1 官方就是这样 |
| DocBook | <para> | <simpara>, <formalpara> | 技术文档系统 |
| Spring Beans | <bean> | <util:list>, <aop:aspect> 等 | Spring 2.x–5.x |
| Maven pom.xml | <plugin> | <build>, <reporting> 下的插件 | Maven 本身 |
| SOAP 消息头 | <soap:Header> | 各种 ws-security、ws-addressing 头 | Web Service 标准 |
| 支付系统 | <payment> | <alipay>, <wechatpay>, <paypal> | 统一支付网关 |
重要规则和坑(90%的人在这里翻车)
| 规则 | 例子说明 |
|---|---|
头元素必须是全局元素(直接位于 <xs:schema> 下) | 正确:<xs:element name="shipTo" ...> 错误:局部元素 |
成员元素必须显式写 substitutionGroup="xxx" | 不能靠类型相同自动替换,必须写这行 |
| 成员类型必须是头元素类型的派生(可以是同一个类型) | 允许 <xs:restriction>、<xs:extension>,也允许相同类型 |
| 在内容模型中只能写头元素(ref=头元素) | 不能直接写成员元素,否则失去多态效果 |
| 可以多级替换(成员又可以是另一个替换组的头) | <a> → <b> → <c> 形成替换链 |
可以用 block="substitution" 禁止某个元素被替换(很少用) | <xs:element name="shipTo" block="substitution"/> |
可以用 abstract="true" 让头元素不能直接出现(推荐!) | <xs:element name="payment" abstract="true" type="PaymentType"/> |
推荐写法(最安全、最清晰):
<!-- 头元素设为 abstract,强制必须用子元素 -->
<xs:element name="payment" type="po:PaymentType" abstract="true"/>
<!-- 成员元素 -->
<xs:element name="alipay" substitutionGroup="po:payment" type="po:AlipayType"/>
<xs:element name="wechat" substitutionGroup="po:payment" type="po:WechatType"/>
<xs:element name="creditCard" substitutionGroup="po:payment" type="po:CardType"/>
这样 <payment> 就永远不会出现在实例文档中,强迫开发者使用具体支付方式。
一句话总结:
元素替换 = 全局头元素 + substitutionGroup属性 + 类型派生
写 XSD 时只要看到“同一个位置可以出现多种不同元素”的需求,99% 就是用 Substitution Group!
需要我帮你把某个具体业务(比如支付、通知、报表)改造成元素替换写法吗?把你的 XML 示例贴出来,10 秒给你最标准的 XSD。