用 AXIOM 促进 XML 处理

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 优化了的可选层。

使用 AXIOM

在典型的 SOAP 引擎中,数据可能以三种不同的方法表示:

  • 序列化形式,如 XML 或二进制 XML。
  • 内存中基于树的对象模型,如 DOM。
  • 专用于特定语言的对象,如 Plain Old Java Object (POJO)。

比如一个 Web 服务的调用。传递给服务提供商的数据可能是用语言专用的对象,对于 Java 技术就是 POJO。调用过程的第一步是将这些对象中的信息项放入 SOAP 信封,构造一个 SOAP 消息。因为 SOAP 消息是 XML 文档,所以 Web 服务还必须将数据项转化成要求的 XML 格式。在内存中表示 XML Infoset 需要构造一个对象树,供对象模型(AXIOM)使用。

从头创建 AXIOM

创建内存对象层次结构的第一步是创建一个对象工厂:

OMFactory factory= OMAbstractFactory.getOMFactory();

AXIOM 允许很多不同的对象工厂实现,但链表是最常用的。一旦建立了工厂,就可以开始构造树了。

比如下面的 XML 片段:



清单 1.Line item 细节
<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 显示了完整的代码片段。



清单 2.通过程序创建 line item
   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 来序列化构造好的元素:



清单 3.序列化 line item
   XMLOutputFactory xof = XMLOutputFactory.newInstance();
   XMLStreamWriter writer = xof.
      createXMLStreamWriter(System.out);
      lineItem.serialize(writer);
      writer.flush();

从已有代码构造 AXIOM

现在看看相反的过程,从数据流建立内存对象模型。

最简单的情况下,只需要关心 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 显示了完整的代码片段。



清单 4.从 XML 文件构建 AXIOM
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 的核心概念之一。但是,要理解缓冲必须在树的延迟构造和 AXIOM API 上下文中来思考。AXIOM 提供多种访问底层 XML Infoset 的 API。上面使用的是基于树的 API,所有其他竞争的对象模型如 DOM 和 JDOM 都提供了这样的 API。但是,AXIOM 还允许通过 SAX 或 StAX API 访问信息。如图 1 所示。



图 1. AXIOM,输入和输出
AXIOM,输入和输出

如果要使用一种 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 所示:



图 2.通过 AXIOM 访问底层的解析器
 通过 AXIOM 访问底层的解析器

但是,如果用户希望再回来访问树的同一部分就可能出现问题。因为解析器已经直接连接了用户,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 访问树的这些部分一样构造树。





回页首


AXIOM 和 StAX

了解这些背景之后,现在看看 AXIO 的 StAX API。该 API 中最重要的方法如下:

(OMElement).getXMLStreamReader();
(OMElement).getXMLStreamReaderWithoutCaching();

通过 StAX API 对某个元素调用第一个方法,可以访问该元素的 XML Infoset,同时缓冲(如果需要)树中未构造的部分以供将来使用。顾名思义,第二个方法用于访问同样的信息,但是通过关闭缓冲机制优化了性能。在编写需要使用数据绑定框架的存根和 skeleton 程序时,这是最有用的方法。

但是请注意,如果在调用上述方法之前已经建立了树,AXIOM 将模拟 StAX 解析器。因此有些树节点的事件是通过模拟而来的,而对于另一些节点则直接连接到底层的解析器。AXIOM 的优点在于这些内部处理对用户是透明的。但是,在切换到原始 API 时,必须指明是否需要缓冲数据。

为了说明 StAX API 的用法,我将展示如何使用 XMLBeans 生成的代码连接到 AXIOM。



清单 5.XMLBeans 生成的订单代码
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



清单 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 和数据绑定

这里讨论 AXIOM 的 SAX API,因为有些数据绑定框架不能使用其他的 API,比如 JAXB。虽然上述情况下使用 SAX 显然不会达到最佳性能,但从 AXIOM 到对象使用 SAX 并没有造成性能损失,因为这一步在任何情况下都是必需的。

如果使用 JAXB,那么存根程序就要使用 SAXOMBuilder 从数据绑定对象建立 AXIOM。清单 7 示范了这个过程。



清单 7. AXIOM 和 JAXB
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 服务、业务过程自动化、移动开发和电信网络管理项目的架构师。

你可能感兴趣的:(apache,数据结构,Web,xml,SOAP)