级别: 中级 Erik Ostermueller (eostermueller@yahoo.com), 高级架构师, Fidelity Information Services
2003 年 12 月 01 日 如果使用方法得当,XML Schema 可以显著地降低执行基本数据验证任务所需的工作量。此外,验证规则集中保存于 XML 模式中,这样有助于帮助用户更好地理解您的系统。不过我们必须使用适当的 XML 结构,这样才能对模式验证器加以利用。本文讨论了正确的 XML 结构,还讨论了在 XML Schema 中定义数据验证规则的最佳及最差实践。
如何才能阻止无效的数据进入您的系统呢?您需要自己编写一些验证例程来进行边界检查么?只要您的系统有 XML 输入,XML Schema 验证器在这方面为您节约的时间就简直不可思议。这里说的既包括 DTD 验证器,也包括 XML Schema 验证器。
DTD 验证器提供基本的结构化验证。当条件符合的时候,验证器就会大声喊出“缺少必要的元素”和“该元素不能重复出现”之类的错误消息。然而,DTD 中缺乏对数据类型、可重用结构、名称空间等信息的描述手段。XML Schema 验证器使用的方法更加柔和,而且弥补了这些方面的不足。
除了手工编码数据验证例程之外,我们还可以考虑通过定义 XML Schema
规则达到相同的目的。在运行的时候,模式验证器会读取这些规则,当遇到违反规则的数据时,验证器就出现红色标志。
这种方法可以带来多种好处。XML 模式规则是与编程语言无关的。其语法是数据驱动的,由 W3C 标准负责管理。这就意味着您可以给验证规则编码,速度比自己编写验证代码快得多。更进一步看,我们更容易在多个不同组件和不同的开发人员之间建立标准的系统验证机制。
此外,XML 模式验证可以极大地改善客户对您的 XML API 的理解。手工编码的数据验证机制对于 API 的使用者来说并不是很容易理解。与验证有关的代码通常散布在多个源文件中,文档也不甚详尽。但是,如果您有了好的可以全面描述数据验证的 XML 模式,您的客户就能更好地看到数据验证规则,也就能更好地理解您的 API。模式中每增加一条新的规则,您的客户不用深入研究您的源代码就可以多了解一点情况。
不过,有一些对 XML 进行结构化的方法可能会妨碍您充分利用模式验证器的优势。下面的内容能够帮助您理解和避免这些问题,这些问题在 DTD 和 XML Schema 中都存在。然后,我会讨论实际定义验证规则时的最佳及最差实践——这些内容是只针对 XML Schema 的,对 DTD 不适用。最后,我将简要讨论一下当前可用的模式验证器中存在的一些局限性。
XML 文法还是 XML 消息集?
您应该已经熟悉下面这样的 XML 文档了:
<ele>
<DataElement variable="CustomerName" value="Smith"/>
<DataElement variable="AccountBalance" value="100.00"/>
<DataElement variable="TransDate" value="12/22/1996"/>
</ele>
|
这个 XML 是完全有效的内容,但是它这种样式可能未必需要很多手工编码的数据验证机制。下面的例子能更好地解释这个问题——假设其中存在一个 bug:
<ele>
<DataElement variable="CustomerName" value="Smith"/>
<DataElement variable="AccountBalance" value="100.00"/>
<DataElement variable="
TansDate" value="12/22/1996"/>
<!-- TYPO! --></ele>
|
您有什么办法来防止这个 bug 进入您的系统呢?也许您必须手工编写验证代码来根除这个 bug,而不关心非法的日期和非法的数字。几乎任何系统输入的数据都需要进行大量的验证工作。而 XML 强制您不仅对数据进行验证,而且要验证变量的名称,因此,程序员需要准确地记住您系统中每一个容易记住的变量名称的正确拼写形式。况且违法的 XML 元素和程序员的输入错误也不是那么容易对付的。现在,我又从新创建了一个 XML 文档:
<ele>
<DataElement variable="" value=""/>
<DataElement variable="" value=""/>
<DataElement variable="" value=""/>
<DataElement variable="" value=""/>
</ele>
|
空值的存在是非常危险的事情。所有这些引号里面都应该是什么东西?这个 XML 文档描述的是一项金融事务,还是水下呼吸器?日期的有效格式是什么?这段 XML 代码中未知的东西太多了。当然了,如果您不介意去自己搜索处理这些数据的代码,我也就没什么好说的。要不是这样,那么想想看,您的员工要回答上面这些问题,会浪费多少时间?
从积极的角度看,这种样式的文档还是具有一点好处的:如果您需要向文档中增加新的变量,或是修改已经使用的变量(比如说增加一个名为
IsCustomerActive 的新变量),您没必要花费时间对控制该文档结构的模式进行修改;您只需要在 XML 中增加一个新的标签,写上新的变量名称,就可以了。
然而,获得这种灵活性所付出的代价相当昂贵——您的系统会更容易受到坏数据的攻击。下面的 XML 文档经过重新整理,解决了这些问题。
<AccountInquiry>
<CustomerName>Smith</CustomerName>
<AccountBalance>100.00</AccountBalance>
<TransDate>12/22/1996</TransDate>
</AccountInquiry>
|
咳,原来这条消息讲的不是水下呼吸器的事情。您现在可以为每一个标签(用明确定义的格式)指派一个数据类型。现在,模式验证器可以负责为前面那个 bug 抛出异常。更进一步说,验证器将承担为非法的日期和数字抛出错误的工作。最终您的客户会感谢您为他提供的这种结构,可以根据行业标准对自身进行完整的描述。
只要您具备足够的创造力,就可以用
<DataElement> 、
variable="" 和
value="" 来定义任意的数据结构。这也就是第一个问题中描述的情况:我们的目标是描述非常特定的商业事务,但是最终得到的却是这种一般的
<DataElement> 结构。一般的 XML 结构与特定的结构之间的区别十分重要。下面我列出这两种 XML 结构的不同样式之间的区别:
- 一般性 XML 结构适合于各种应用程序,它是一种 XML 文法。
- 特定的 XML 文档可根据某一个目标进行调节,它是 XML 消息集的一部分。
XML 文法和 XML 消息集是完全不同的东西。每一个模式都会侧重描述其中的一种。一般来说,模式对实例文档设置限制。然而 XML 文法的模式却给用户保留了相当大的创造空间。它们是相当开放的底层模式,对这些模式,您可以找到的合法实例文档可能不计其数。同是 XML 文法,合法的 XML 从尺寸和形式上都可能有很大的不同,却依然符合这种模式。这样的例子成百上千,我们以 MathML 和 UIML 为例(参见
参考资料)。MathML 使您能够对数学等式进行建模。您能创建多少种不同的数学公式呢?UIML 也与之类似,想想您可能创建多少种 UI 应用程序吧。
另一方面,消息集的模式中可以发挥的空间就很小。想像一下,您能用上面例子中那四个标签创建多少种不同的帐目查询?只有税费收集器才能从中找到发挥的余地。消息集的设计目标是进行严格的验证。同一消息的有效实例文档看起来都惊人地相似。假设有一条合法的消息,它来自 Interactive Financial Exchange(IFX)消息集,这是金融领域内的事务建模。合法的用户更新消息大体包含相同的标签组。另一个消息集合的例子是 Open Travel Alliance(OTA)。(有关 IFX 和 OTA 的更多信息请参阅
参考资料。)
有关这个问题的经验是,不要用 XML 文法来描述限制性较强的东西:在这种情况下应该使用 XML 消息集。如果使用了文法,您就会陷入自己手工编写大量数据验证代码的泥潭。
规则中的例外情况?
大家都说,含糊不清的数据也可能有价值。比如说,SOAP 消息的模式就是应用程序定义的一种消息而已。它使用了 XML Schema 中的
any 特性,如下所示:
<complexType name="Body">
<!-- This is a placeholder for your message payload.-->
<!-- Use a WSDL file to define the structure. -->
<any minOccurs="0" maxOccurs="*" />
<anyAttribute/>
</complexType>
|
这种结构的作用是对人们进行提示。它似乎是在说:“嘿、嘿,把应用程序专用的东西放在这里。”从另一方面来看,这种结构也没有让人们觉得模式验证十分痛苦。不管怎样,SOAP 提供了另外一种机制,可以为上面的
Body 元素指定结构和验证规则,这就是 WSDL 文件。还有一些组件,如 SOAP 服务器,必须处理其他的 XML 结构。不过请别把这个误解成暂缓使用模式验证的借口。请您遵从 SOAP/WSDL 中的这种模式,这样在获得灵活性的同时,也不会损失模式验证的特性。要了解这个问题更加彻底的解决方法,请您阅读 Dare Obasanjos 关于 XML Schema 灵活性的文章(参阅
参考资料)。
小心属性和元素文本
我们对前面的 XML 进行了改写,如下所示:
<Message msgType="AccountInquiry">
<CustomerName>Smith</CustomerName>
<AccountBalance>100.00</AccountBalance>
<TransDate>12/22/1996</TransDate>
</Message>
|
我们改变了什么?根元素不再是
<AccountInquiry> ,而是
<Message msgType="AccountInquiry"> 。如果您想让模式验证器为您进行验证的话,这个地方就是一个错误,下面告诉您原因。我们在同一个消息集中再建立一个消息模型
ProductInquiry ,这下问题就显而易见了:
<Message msgType="ProductInquiry">
<ProductName>DEPOSIT</ProductName>
<BankName>Freds Bank</BankName>
<TransDate>12/22/1996</TransDate>
</Message>
|
<Message> 元素具有两种不同的含义,其一代表帐户,其二代表产品。太混乱了吧!首先,我宣称
<Message> 元素必须包含帐目信息,且只包含帐目信息。然后,我改变了主意,宣称同一个
<Message> 元素现在必须包含产品信息,而且只包含产品信息。
<Message> 的模式中必须包含全部的帐目和产品元素,以供所有的消息选择使用。如果所有的东西都是可选的,那么假设当产品消息中缺少重要的产品数据时,验证器就会停止工作。您为了避免自己手工编写验证代码,可能想让验证器在数据缺失的时候发出警报声。当我在属性中编写消息类型的代码时,像这样:
msgType="ProductInquiry" ,所有的问题就出现了。当您用编码的方式处理元素文本中的相同信息时,也会陷入同样的困境。如下所示:
<MessageType>AccountInquiry<MessageType>
<Message>
<CustomerName>Smith</CustomerName>
<AccountBalance>100.00</AccountBalance>
<TransDate>12/22/1996</TransDate>
</Message>
|
这段代码还是重载了
<Message> 元素。这种做法并不总是坏事,但是在本文的例子中却无意中妨碍了模式验证器的工作。为了摆脱这种进退两难的局面,您需要将
<Message> 换成
<AccountInquiry> 或
<ProductInquiry> 。然后,在模式中为每这两个元素都指定适当的子元素。
我们的底线就是要在使用属性和元素文本的时候小心谨慎。如果您想要指定消息中需要和不需要哪些内容,那这两样东西都是没有用的。本文还将引导您拨开迷雾,了解重载类似
<Message> 这样的元素到底意味着什么。
属性并不完全代表罪恶。不过,告诉您什么时候不要使用属性似乎更加容易一些。请您记住这句话:模式验证器的主要职责是进行结构化验证;属性在这里面并不会起到多大的作用。验证器很少会根据属性的值来确定 XML 文档的其他部分是否有效(在这个问题上比较含混的是 XML Schema 中的
Identity 和
KeyRef 这两个特性)。这样的安排使得属性在模式验证中扮演了二流角色。对于元素文本来说情况也一样。
有一些技术使用了其他一些模式语言,尽管不是很常用,但是能够更好地处理属性和元素文本。(请参阅
参考资料中 Jeni Tennison、Roger L. Costello 以及 Bob Ducharme 的相关著作)。
请您记住 XML 文法和 XML 消息集之间的区别。了解这些区别有助于您书写结构化的 XML,以便利用模式验证器的优势。还有一点请您记住,XML 属性在模式验证中的应用很有限。
到目前为止,我已经讨论了如何将 XML 格式化,以便为使用模式验证器铺平道路。本文的剩余内容将着重探讨定义 XML Schema 验证规则的最佳实践。从这里开始,我们讨论的对象就是 XML Schema(请您注意,“Schema”一词第一个字母是大写的)。
不要局限于 UML
多年以来,软件开发人员一直在构建业务消息的模型。在 XML 出现之前,我工作过的一些组织曾经用 UML(Unified Modeling Language,统一建模语言)来建立业务消息模型。然而,在 UML 中,有一些东西一直是缺失的。大多数 UML 标记仅仅是文档而已。如果您想要验证您建立的模型中的约束,您就必须自己编写验证代码。您要把这些约束编写两次,第一次用于标记,第二次用于编码,这可真是件痛苦的事情。
XML 模式解决了这个问题。您仅仅通过增加标记,就可以获得运行时的验证机制。在 XML Schema 中,您很容易就可以对下面这些验证规则进行建模,从而展示出业务消息中的很多信息:
- 一段数据天生具有的必需或是可选的属性。
- 数据长度:例如,帐目编码必须具备指定长度、最小长度或者最大长度。
- 通配符掩码:例如,帐目编码必须由两个字母加任意数量的数字组成。
- 枚举与数字范围:例如,某个特定字符串属性必须为 A、C 或者 D。再比如,某个特定的数字必须在 1 至 500 之间。
- 多重性:您可以指派两个实体之间的关系具有多重性。例如,每一节篮子编织课程上必须有 20 到 30 名学生。如果您在开发阶段加入这种类型的约束,在运行的时候模式验证器就会拒绝无效数据。如果第 31 名学生试图进入课堂,模式验证器就会引发一个异常。
您只需要将这些规则加入模式中,就自动获得了文档验证和运行时验证的能力。这样的买卖还真是不错。单独使用 UML 并不能实现这样的事情。事实上,您必须通过 UML 晦涩的对象约束语言(Object Constraint Language,OCL),并使用 OCL 代码生成器,才能实现相同的功能。有关 OCL 的更多信息,请参阅
参考资料。
使用 XML Schema 的最佳及最差实践
必需及可选元素建模
用 XML Schema 为您的事务中必需的和可选的元素建模。
好的方法:如果某个元素在您的业务消息中是必需的,那么就用 XML Schema 对其建模。这样做可以启用模式验证,这样当坏数据进入系统时就能标记出来,您没有必要在这件事情上花费精力。同时也能为客户机提供有关这方面的文档:
<xsd:element
name="FirstName"
type="xsd:string"
minOccurs="1"/>
|
不好的方法:有时候模式中会指示数据项为可选,但系统中其他的部分却可能因为这部分数据的缺失而失败。下面是 XML Schema 中的一个可选元素:
<xsd:element
name="FirstName"
type="xsd:string"
minOccurs="0"/>
|
首先,整个过程背后的逻辑是合理的:如果您将这项数据元素从可选改为必需,那么您基本上就是复制了系统中其他地方所使用的错误处理过程。然而如果您这样做了,使用您的 API 的人就会把很多时间都浪费在查找这个错误上。考虑一下下面的情况:
- 用户准备好提交事务,但其中没有该数据项(模式中没有指出这一项是必需的)。
- 系统中另外一部分报错。
- 用户通过理解错误消息,最终发现一项必需的元素缺失了。
- 用户将这个必需的数据项加入进来,然后重新提交事务。
- 用户验证系统运行正常。
您可以在模式中标明哪些元素是必需的,这样就能避免大多数情况。
日期/时间数据类型建模
如果要在您的业务消息中使用日期和时间数据,请使用
xsd:dateTime 。
好的方法:日期的格式在 XML Schema 规范中定义得很好。
<xsd:element name="BirthDate" type="xsd:dateTime"/>
|
不好的方法:下面清单中列出的是不好的方法,因为这种方法强制用户为日期指定正确的格式。
<xsd:element name="BirthDate" type="xsd:string"/>
|
模式文档
好的方法:用 XML Schema 注释节点进一步描述业务消息中的元素以及业务消息本身。如下例所示:
<xsd:element name="Amount" type="xsd:integer">
<xsd:annotation>
<xsd:documentation>
Use this element to specify
the amount that should be transferred
</xsd:documentation>
</xsd:annotation>
</xsd:element>
|
这种类型的文档有助于创建模式和 XML 实例文档。
不好的方法:如果您不给模式编写文档,用户就面临两种选择——他们可能去搜索您的源代码,或是去寻找单独的文档文件。第一种方法尽管是最精确的,但是源代码中隐藏的那一点点宝藏可能要花费很多时间才能找出来。维护独立于模式的文档则比写在模式内部的文档更容易脱离实际情况。
相互排斥的数据项建模
好的方法:很多业务消息中都包含存在相互排斥关系的数据项。比如说,在一个消息实例中,某两个(或者多个)元素之中只能存在一个。下面是用 XML Schema 元素描述这些数据项的方法:
<xsd:choice>
<xsd:element name="DestinationAccount" type="AccountKeyType"/>
<xsd:element name="SourceAccount" type="AccountKeyType"/>
</xsd:choice>
|
现在,就只能出现下面两种 XML 代码了:
<MyMessage>
<DestinationAccount>
</MyMessage>
|
或者
<MyMessage>
<SourceAccount>
</MyMessage>
|
模式验证器会拒绝下面的代码:
<MyMessage>
<!-- fails, because only DestinationAccount
or SourceAccount is allowed -->
<DestinationAccount>
<SourceAccount>
</MyMessage>
<MyMessage>
<!-- fails, because neither DestinationAccount
nor SourceAccount are present -->
</MyMessage>
|
不好的方法:当您对 XML Schema 语法不熟悉的时候,就很容易出现回避它们的情况。但是如果您这样做了,您将不得不自己在业务消息中编写代码,实现相互冲突的数据元素。这种做法还隐藏了数据的一个重要性质。只有能访问源代码的人才可能知道这种互斥的情况。
枚举
好的方法:XML Schema 枚举可以帮助用户发现某个特定变量的有效值。
<xsd:element name="TransactionIndicator">
<xsd:simpleType>
<xsd:restriction base="xsd:string">
<xsd:enumeration value="FORCE_POST"/>
<xsd:enumeration value="BACKDATE"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:element>
|
如果您没有选择这种方法,而是将某个特定变量的有效值保存在数据源中(如 LDAP 或 RDBMS),您就应该在 XML Schema 文档中确切地定义出如何枚举这些有效值。
<xsd:element
name="TransactionIndicator"
type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Use business message "TransactionIndicatorInquiry"
to discover the valid values for this attribute.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
|
不好的方法:用缩写表示枚举数据不是个好办法,因为这样用户就无法判断该用哪一个值了。
<xsd:element name="TransactionIndicator">
<xsd:simpleType>
<xsd:restriction base="xsd:string">
<xsd:enumeration value="F"/>
<xsd:enumeration value="B"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:element>
|
更不好的方法:
如果某个变量具有固定的一组有效值,那么不要隐藏这些值,像下面这样:
<xsd:element
name="TransactionIndicator"
type="xsd:string"/>
|
这样用户就不得不寻求别人的帮助、进行试验或者是搜索其他的文档或源代码。
模式验证的不足之处
不仅是 XML Schema,大多数模式验证器都可以为您的应用程序增加极大的价值。但是,模式验证器一般说来都有其共同的问题。对于初学者而言,模式验证器的消息是具有描述性的,但是对用户并不友好。下面就是一个例子:
[Error] greetings.xml:1:12: Element type "greetings" must be declared.
|
这可不是能展示给最终用户的消息。更进一步说,即便是有,也只有很少的验证器可以用多种语言(如英语、汉语、法语等)发出错误消息。同样,对于日期、时间、货币数量等的错误消息也没有本地化。同时,大多数验证器在发现第一个错误之后就不再报告错误了。如果能一下子获知所有的错误就好了。最后一点,模式验证的性能依然是个问题。数据绑定机制,如 Castor、XSD、JAXB,有助于解决这些性能问题。即便是在不使用 XML 接口的情况下,这些机制也可以帮助您使用模式验证。现在,您甚至可以将 Castor 模式验证功能同 Apache Axis 结合起来(参阅
参考资料),构成一种了不起的 SOAP 系统。
结束语
如果您用模式(特别是用 XML Schema)来描述您的数据结构,有很多很好的第三方工具可以帮助您走出困境。然而,您必需花费一些力气对 XML 消息进行结构化,这样模式验证器才能够发挥作用。如果您遵从了本文中介绍的 XML 样式准则,您至少可以达到一箭双雕的目的。首先,您将一些数据验证的责任从自己编写代码实现转为用模式验证器实现,这样能够节约您的时间和金钱。其次,创建 XML 的人能够知道有关您的 XML 接口的更多信息。如果您不能够很好地描述您的接口,那就只能看着这些人在您的办公桌前排起长队,轮流浪费您宝贵的时间了。
致谢
很多人都为这篇文章作出过贡献,在此一并感谢。我特别要感谢 FNF 的高级技术作家兼编辑 Colin Reeves,她的编辑经验使本文增色不少。
参考资料
关于作者  | |  | Erik Ostermueller 十年以来一直是首席软件开发人员和顾问。他在美国和欧洲都有相关的经验。目前他受雇于 Fidelity Information Services。Erik 已经在两个不同的会议上发表有关 XML 的演讲。他积极参与到几个 Java 的开放源代码项目中,他关心的重点是 XML Schema、自动化测试、Unicode 以及可用性方面的问题。您可以通过
eostermueller@yahoo.com 与他取得联系。
|
对本文的评价
|