AXis 对象模型是一种强大的 XML 处理新方法
2005 年 10 月 17 日
AXis 对象模型(AXis Object Model,AXIOM)是 Apache Axis 2 的 XML 对象模型,其目标是提供强大的特性组合彻底改变 XML 处理技术。AXIOM 超越了现有的 XML 处理技术,它把延迟构建和一种快速、轻型的可定制对象模型结合了起来。本文中,软件架构师、AXIOM 的首创者 Eran Chinthaka 介绍了这种新的 XML 处理方法。
AXIOM 还不是另一种对象模型。它有着明确的设计目标:大幅提升 Apache 下一代 SOAP 协议栈 Axis 2 的性能。结果造就了不同于其他对象模型的 AXIOM(也称为 OM),因为它突出了构造的轻型,并且 仅当需要的时候才建立。由于是轻型的,它尽可能地减轻对系统资源的压力,特别是 CPU 和内存。同时,延迟构造又允许在其他部分还没有完成的时候使用树的一部分。AXIOM 强大的延迟构建能力源于底层的 Streaming API for XML (StAX) 解析器。AXIOM 提供了所有这些特性,同时幕后的复杂性对用户是透明的。
使用 XMLBench Document Model Benchmark 测试(请参阅 参考资料)的结果表明,AXIOM 的性能和现有的高性能对象模型相当。但是 AXIOM 的内存占用要好于现有多数依靠 SAX 和/或 DOM 输入输出的对象模型。因此对于 Web 服务引擎或内存受限制设备这样的 XML 处理器,AXIOM 是一种理想的选择,它可用于一般的 XML 处理,但是有一个对 SOAP 优化了的可选层。
在典型的 SOAP 引擎中,数据可能以三种不同的方法表示:
比如一个 Web 服务的调用。传递给服务提供商的数据可能是用语言专用的对象,对于 Java 技术就是 POJO。调用过程的第一步是将这些对象中的信息项放入 SOAP 信封,构造一个 SOAP 消息。因为 SOAP 消息是 XML 文档,所以 Web 服务还必须将数据项转化成要求的 XML 格式。在内存中表示 XML Infoset 需要构造一个对象树,供对象模型(AXIOM)使用。
创建内存对象层次结构的第一步是创建一个对象工厂:
OMFactory factory= OMAbstractFactory.getOMFactory(); |
AXIOM 允许很多不同的对象工厂实现,但链表是最常用的。一旦建立了工厂,就可以开始构造树了。
比如下面的 XML 片段:
<po:line-item po:quantity="2" xmlns:po="http://openuri.org/easypo"> <po:description> Burnham's Celestial Handbook, Vol 2 </po:description> <po:price>19.89</po:price> </po:line-item> |
注意,所有的元素和属性都属于 "http://openuri.org/easypo"
名称空间。因此,为这个 XML 片段构造 AXIOM 树的第一步就是创建名称空间,如下所示:
OMNamespace poNs= factory.createOMNamespace("http://openuri.org/easypo", "po"); |
现在可以构造包装器元素 line-item
了:
OMElement lineItem= factory.createOMElement("line-item", poNs); |
接下来创建 line-item
元素相关的子元素和属性。
最好用下面的方式创建元素属性:
lineItem.addAttribute("quantity", "2", poNs); |
与其他元素一样创建子元素,然后按照下面的方式结合到父元素中:
OMElement description= factory. createOMElement("description", poNs); description.setText("Burnham's Celestial Handbook, Vol 2"); lineItem.addChild(description); |
类似地,也添加 price
子元素:
OMElement price= factory.createOMElement("price", poNs); price.setText("19.89"); lineItem.addChild(price); |
清单 2 显示了完整的代码片段。
OMFactory factory = OMAbstractFactory.getOMFactory(); OMNamespace poNs = factory.createOMNamespace("http://openuri.org/easypo", "po"); OMElement lineItem = factory.createOMElement("line-item", poNs); lineItem.addAttribute("quantity", "2", poNs); OMElement description = factory.createOMElement("description", poNs); description.setText("Burnham's Celestial Handbook, Vol 2"); lineItem.addChild(description); OMElement price = factory.createOMElement("price", poNs); price.setText("19.89"); lineItem.addChild(price); |
现在可以使用 StAX writer 来序列化构造好的元素:
XMLOutputFactory xof = XMLOutputFactory.newInstance(); XMLStreamWriter writer = xof. createXMLStreamWriter(System.out); lineItem.serialize(writer); writer.flush(); |
现在看看相反的过程,从数据流建立内存对象模型。
最简单的情况下,只需要关心 XML 片段的反序列化。但是在 SOAP 处理中,需要反序列化 SOAP 消息或者经过 MTOM 优化的 MIME 信封。因为与 SOAP 处理关系特别密切,所以 AXIOM 为此提供内置支持,稍候将详细介绍。但首先要说明如何反序列化简单的 XML 片段,具体来说就是刚刚序列化的那个 XML 片段。
首先构造一个解析器。AXIOM 支持用 SAX 和 StAX 解析器解析 XML。但是,SAX 解析不允许对象模型的延迟构造,因此在延迟构建很重要时,应该使用基于 StAX 的解析器。
第一步是为数据流获得一个 XMLStreamReader
:
File file= new File("line-item.xml"); FileInputStream fis= new FileInputStream(file); XMLInputFactory xif= XMLInputFactory.newInstance(); XMLStreamReader reader= xif.createXMLStreamReader(fis); |
然后创建一个 builder 并将 XMLStreamReader
传递给它:
StAXOMBuilder builder= new StAXOMBuilder(reader); lineItem= builder.getDocumentElement(); |
现在可以使用 AXIOM API 来访问属性和子元素或者 XML Infoset 项了。可以这样访问属性:
OMAttribute quantity= lineItem.getFirstAttribute( new QName("http://openuri.org/easypo", "quantity")); System.out.println("quantity= " + quantity.getValue()); |
用类似的方式访问子元素:
price= lineItem.getFirstChildWithName( new QName("http://openuri.org/easypo", "price")); System.out.println("price= " + price.getText()); |
清单 4 显示了完整的代码片段。
File file = new File("line-item.xml"); FileInputStream fis = new FileInputStream(file); XMLInputFactory xif = XMLInputFactory.newInstance(); XMLStreamReader reader = xif.createXMLStreamReader(fis); StAXOMBuilder builder = new StAXOMBuilder(reader); OMElement lineItem = builder.getDocumentElement(); lineItem.serializeWithCache(writer); writer.flush(); OMAttribute quantity = lineItem.getFirstAttribute( new QName("http://openuri.org/easypo", "quantity")); System.out.println("quantity= " + quantity.getValue()); OMElement price = lineItem.getFirstChildWithName( new QName("http://openuri.org/easypo", "price")); System.out.println("price= " + price.getText()); OMElement description = lineItem.getFirstChildWithName( new QName("http://openuri.org/easypo", "description")); System.out.println("description= " + description.getText()); |
AXIOM 最好的一点是,努力在延迟构造这类高端技术上提供用户友好的 API。但是要充分发挥其潜能,必须了解底层体系结构。
|
缓冲是 AXIOM 的核心概念之一。但是,要理解缓冲必须在树的延迟构造和 AXIOM API 上下文中来思考。AXIOM 提供多种访问底层 XML Infoset 的 API。上面使用的是基于树的 API,所有其他竞争的对象模型如 DOM 和 JDOM 都提供了这样的 API。但是,AXIOM 还允许通过 SAX 或 StAX API 访问信息。如图 1 所示。
如果要使用一种 XML 解析 API,为何还要构造对象模型呢?为了使用不同 API 访问对象模型的不同部分。比如,考虑 SOAP 栈的情况:SOAP 消息在被目标服务消费之前可能会经过多个处理程序的处理。这些处理程序通常使用基于树的 API(特别是 SOAP with Attachments API for Java,或 SAAJ)。服务实现还可能使用数据绑定工具将 SOAP 消息负荷中的 XML 文档转化成对象,如 POJO。因为用户不使用基于树的对象模型来访问这部分文档,所以构造完整的树会因为数据重复而浪费内存。最直接的解决方法是向数据绑定工具公开底层的原始 XML 流。这就是 AXIOM 的闪光之处。
为了获得最佳的性能和内存使用,需要让数据绑定工具直接访问底层的 XML 流。AXIOM 完全允许这样做。延迟构建仅仅意味着只有在访问的时候才构造要访问的这部分树。因此如果不需要访问 SOAP 消息体,SOAP 消息的这部分就不会被构建。如果用户开始使用 SAX 或 StAX 访问消息体,而它还没有构建,AXIOM 将把用户直接连接到底层的解析器,以便提供最佳的性能。如图 2 所示:
但是,如果用户希望再回来访问树的同一部分就可能出现问题。因为解析器已经直接连接了用户,AXIOM 退出了,就是说所有信息都从低层的流直接流向用户。因此当用户回来请求同样的信息时,无论第二次选择什么样的 API,AXIOM 都不能提供该信息。注意这两种可能性差不多相等。比如,多数情况下 SOAP 体的处理中只有最终的服务实现才会涉及到负荷。服务可以使用数据绑定或其他 XML 处理 API 如 SAX、StAX 或 XPath 来处理消息体。这种情况下,消息体很少被访问两次,AXIOM 提供的优化具有最好的性能。
但是,假设在处理程序链中插入一个日志处理程序,使用 StAX writer 记录整个 SOAP 消息。如果服务实现尝试访问消息体,而消息体不存在!
为了进一步说明这一点,下面是一个比较简单的例子,虽然有点牵强。
StAXOMBuilder builder = new StAXOMBuilder(reader); lineItem = builder.getDocumentElement(); lineItem.serialize(writer); writer.flush(); price = lineItem.getFirstChildWithName( new QName("http://openuri.org/easypo", "price")); System.out.println("price= " + price.getText()); |
由于延迟构造,获得 lineItem
元素的时候该元素还没有构造完成。因此后面使用 StAX writer 进行序列化时,AXIOM 把 StAX writer(它序列化 lineItem
元素)直接连接到 StAX reader(它最初被传递给 builder
)。但是这个过程中,AXIOM 断开了自身和数据流的连接。现在当请求 price
子元素的时候,找不到这样的元素,因为 lineItem
的所有子元素都在序列化器中消失了。
这种情况下,惟一的办法是避免序列化过程中 AXIOM 完全和数据流脱离开。用 AXIOM 的术语称为缓冲:无论是否在内存中建立了对象模型,AXIOM 都允许获得 StAX 事件或者 序列化 XML。因此,AXIOM 把策略(比如是否应该缓冲消息)和机制(如何缓冲)分离开来。它允许用户在开始使用原始 XML 处理 API(如 SAX 或 StAX)时决定是否缓冲树中未用到的部分以供将来引用。如果用户决定这样做,当树构造完成时可以再回来访问这些部分。但是,用户必须付出内存占用和性能的代价。另一方面,如果用户了解自己的目标,并确信只此一次需要访问树的这些部分,则可以选择关闭 缓冲来充分发挥 AXIOM 的效率。
因此,上一段代码应改写为:
StAXOMBuilder builder = new StAXOMBuilder(reader);
lineItem = builder.getDocumentElement();
lineItem.serializeWithCache(writer);
writer.flush();
price = lineItem.getFirstChildWithName(
new QName("http://openuri.org/easypo", "price"));
System.out.println("price= " + price.getText());
|
方法 serializeWithCache
与对应的 serialize
不同,不会将 StAX reader 直接连接到 StAX writer。相反,从 reader 传递给 writer 的所有数据都保留 在 AXIOM 中。具体如何缓冲与用户无关。目前如果启用缓冲,AXIOM 就会像用户在通过文档 API 访问树的这些部分一样构造树。
|
了解这些背景之后,现在看看 AXIO 的 StAX API。该 API 中最重要的方法如下:
(OMElement).getXMLStreamReader(); (OMElement).getXMLStreamReaderWithoutCaching(); |
通过 StAX API 对某个元素调用第一个方法,可以访问该元素的 XML Infoset,同时缓冲(如果需要)树中未构造的部分以供将来使用。顾名思义,第二个方法用于访问同样的信息,但是通过关闭缓冲机制优化了性能。在编写需要使用数据绑定框架的存根和 skeleton 程序时,这是最有用的方法。
但是请注意,如果在调用上述方法之前已经建立了树,AXIOM 将模拟 StAX 解析器。因此有些树节点的事件是通过模拟而来的,而对于另一些节点则直接连接到底层的解析器。AXIOM 的优点在于这些内部处理对用户是透明的。但是,在切换到原始 API 时,必须指明是否需要缓冲数据。
为了说明 StAX API 的用法,我将展示如何使用 XMLBeans 生成的代码连接到 AXIOM。
public class PurchaseOrderSkel { public void submitPurchaseOrder( PurchaseOrderDocument doc) throws Exception { } public void submitPurchaseOrderWrapper( OMElement payload) { try { XMLStreamReader reader= payload. getXMLStreamReaderWithoutCaching(); PurchaseOrderDocument doc = PurchaseOrderDocument.Factory.parse(reader); submitPurchaseOrder(doc); } catch (Exception ex) { ex.printStacktrace(); } } } |
清单 5 中的代码(通常用代码生成工具生成)展示了一个 skeleton,它使用 XMLBeans 生成的类(即 PurchaseOrderDocument
)进行数据绑定。这个 skeleton 包含两个服务实现方法。第一个允许服务实现者使用数据绑定对象,第二个则允许直接访问 AXIOM API。主要看看这几行:
XMLStreamReader reader= payload. getXMLStreamReaderWithoutCaching(); PurchaseOrderDocument doc = PurchaseOrderDocument.Factory.parse(reader); |
为了创建对象,首先对 SOAP 栈(如 Apache Axis)压入服务实现的载荷获得对 StAX API 的引用。因为现在在处理链的最末端,所以可以安全地把解析器直接连接到 XMLBeans 解除封送器以获得最佳性能。
对于 清单 5 中的 skeleton,其存根代码类似于 清单 6。
public class PurchaseOrderStub { public void submitPurchaseOrder( PurchaseOrderDocument doc) throws Exception { SOAPEnvelope envelope = factory.getDefaultEnvelope(); XMLStreamReader reader = doc.newXMLStreamReader(); StAXOMBuilder builder = new StAXOMBuilder(reader); OMElement payload= builder.getDocumentElement(); envelope.getBody().addChild(payload); // ... } } |
主要看看这几行:
XMLStreamReader reader = doc.newXMLStreamReader(); StAXOMBuilder builder = new StAXOMBuilder(reader); Element payload= builder.getDocumentElement(); |
从这段代码可以看出,经过 StAX API 从对象到 AXIOM,与从 XML 到 AXIOM 没有什么区别。
但是初看起来不那么明显的是延迟构造仍然在起作用!即使在将载荷插入 SOAP 信封的过程中创建了 OMElement
,内存中也没有重复的信息项。这是由于延迟构造和 AXIOM 内的多路技术造成的,它将从一个 API 输入的数据直接转发给另一个 API 输出。当消息最终写入流的时候,XMLBeans 提供的 XMLStreamReader
直接连接到传输 writer,后者将消息写入套接字 —— 假设此过程中没有要查看消息的处理程序。这意味着直到此时,数据仍然存放在 XMLBeans 对象中,真是好极了!
|
这里讨论 AXIOM 的 SAX API,因为有些数据绑定框架不能使用其他的 API,比如 JAXB。虽然上述情况下使用 SAX 显然不会达到最佳性能,但从 AXIOM 到对象使用 SAX 并没有造成性能损失,因为这一步在任何情况下都是必需的。
如果使用 JAXB,那么存根程序就要使用 SAXOMBuilder
从数据绑定对象建立 AXIOM。清单 7 示范了这个过程。
public class PurchaseOrderStub { public void submitPurchaseOrder( PurchaseOrder doc) throws Exception { SOAPEnvelope envelope = factory.getDefaultEnvelope(); SAXOMBuilder builder = new SAXOMBuilder(); JAXBContext jaxbContext = JAXBContext.newInstance("po"); Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.marshal(doc, builder); OMElement payload= builder.getDocumentElement(); envelope.getBody().addChild(payload); //... } } |
到目前为止,AXIOM 还不允许使用 OMElement
注册内容处理程序来处理收到的 SAX 事件。不过很容易编写一段胶水代码,从提供的 StAX 接口接收事件并驱动 SAX ContentHandler
。有兴趣的读者可以从 参考资料 中的 JAXB 参考实现中找到这样的实现。
|
我介绍了与典型的 XML 对象模型相比 AXIOM 引入的一些很有前途的特性。注意本文仅仅介绍了部分特性。AXIOM 有很多更强大的特性,建议您从 Axis 2 源代码库(请参阅 参考资料)下载最新的源代码,进一步研究 AXIOM。
Eran Chinthaka 是首创 Apache Axis 2 项目的架构师,专职是 Lanka Software Foundation 的软件工程师。他曾经为 Axis 2 实现了 AXIOM、WS-Addressing、SOAP 1.1 和 1.2,以及 BPEL4WS 的可视化建模工具。此外,他还做过 Web 服务、业务过程自动化、移动开发和电信网络管理项目的架构师。 |