脈絡:許多穿過整合方案的訊息由多個元素組成。例如客戶下的一張訂單不只一項商品,而如同 Content-Based Router 所述,每一項商品可能得由不同的庫存系統處理。因此我們需要一種作法:處理一整張訂單,卻個別對待其中的每一項商品

解法必須夠通用#

這個路由問題的解法必須通用到能應付數量與型別都會變化的元素。訂單可以有任意多項商品,所以不能假設固定的項數;對訊息含有哪些型別的商品也不該做太多假設——若 Widget & Gadget 公司明天開始賣書,我們希望對整體方案的衝擊降到最低

幾條走不通的路#

用 Publish-Subscribe Channel 把整張訂單送給每個系統,讓它們各自挑走能處理的項目#

這與 Content-Based Router 中討論過的缺點相同:很難避免個別商品漏出或重複出貨

效率考量#

解法也該有效率地使用網路資源。把完整的訂單訊息送給每個「只處理其中一部分」的系統,會造成額外的訊息流量,尤其目的地數量增加時。

把訊息拆成「每個庫存系統一則」#

我們可以把原始訊息拆成與庫存系統數量相同的訊息,每則只含該系統能處理的品項。這種作法類似 Content-Based Router,只是我們先分割、再路由個別訊息

這樣是有效率,但把方案綁死在「特定商品類型與其對應目的地」的知識上。要改路由規則怎麼辦?我們就得去改那個更複雜的「itemrouter」元件。

我們採用 Pipes and Filters 架構,本來就是為了把處理拆成定義良好、可組合的元件,而不是把多個功能揉成一團——這裡也該善用這個架構。

解法#

Splitter 消費一則含有重複元素清單的訊息,為原始訊息中的每個元素(或元素的子集)各發布一則訊息。

讓子訊息自我完備#

許多情況下,我們希望在每則結果訊息中重複某些共同元素。這些額外元素讓子訊息自我完備,因而能被無狀態地處理,也讓稍後調解相關子訊息成為可能。

例如每則訂單項目訊息都該含有一份訂單編號的副本,好把項目正確地關聯回訂單、以及訂單所關聯的其他實體(如下單的客戶)。

迭代式分割器#

許多企業整合系統把訊息資料存成樹狀結構。樹狀結構的美妙之處在於它是遞迴的:某節點底下的每個子節點,都是另一棵子樹的根。

這讓我們能取出訊息樹的某些片段,把它們當作獨立的訊息樹繼續處理。使用訊息樹時,Splitter 可以很容易地被設定成走訪指定節點底下的所有子節點,並為每個子節點送出一則訊息

這樣的 Splitter 實作完全通用——它對子元素的數量與型別不做任何假設。許多商業 EAI 工具以 IteratorSequencer 之名提供這類功能;為了避免廠商詞彙造成混淆,本書稱這種風格為 Iterating Splitter(迭代式分割器)

靜態分割器#

Splitter 的用途不限於重複元素。大訊息也可以被拆成個別訊息以簡化處理

許多 B2B 資訊交換標準規定了極其龐大的訊息格式——這些巨型訊息往往是「委員會設計」的產物,大部分內容其實很少用到。

把巨型訊息拆成各自聚焦於某一部分的個別訊息,好處是:

  • 後續的轉換好寫得多
  • 節省網路頻寬——我們可以把較小的訊息路由給只處理其中一部分的元件

結果訊息通常被發布到不同的通道而非同一個,因為它們代表不同的子型別。這種情況下結果訊息的數量通常是固定的,而較一般化的 Splitter 則假設項目數量可變。本書稱這種風格為 Static Splitter(靜態分割器)

Static Splitter 在功能上等價於「一個廣播通道 + 一組 Content Filter」。

有序或無序的子訊息#

有些情況下,替子訊息加上序號能改善訊息的可追蹤性,並簡化 Aggregator 的工作。

另外,替每則訊息加上指向原始(合併)訊息的參考也是好主意——這樣個別訊息的處理結果才能關聯回原始訊息。這個參考的作用就是 Correlation Identifier

若使用了訊息信封(見 Envelope Wrapper),每則新訊息都應配上自己的信封,以符合訊息基礎設施的要求。例如若基礎設施要求訊息表頭帶時間戳,我們就該把原始訊息的時間戳傳播到每則新訊息的表頭。

範例:用 C# 分割 XML 訂單文件

假設進站訂單長這樣:

<order>
   <date>7/18/2002</date>
   <ordernumber>3825968</ordernumber>
   <customer>
       <id>12345</id>
       <name>Joe Doe</name>
   </customer>
   <orderitems>
       <item>
           <quantity>3.0</quantity>
           <itemno>W1234</itemno>
           <description>A Widget</description>
       </item>
       <item>
           <quantity>2.0</quantity>
           <itemno>G2345</itemno>
           <description>A Gadget</description>
       </item>
   </orderitems>
</order>

Splitter 應產生兩則訊息:

<orderitem>
   <date>7/18/2002</date>
   <ordernumber>3825968</ordernumber>
   <customerid>12345</customerid>
   <quantity>3.0</quantity>
   <itemno>W1234</itemno>
   <description>A Widget</description>
</orderitem>
<orderitem>
   <date>7/18/2002</date>
   <ordernumber>3825968</ordernumber>
   <customerid>12345</customerid>
   <quantity>2.0</quantity>
   <itemno>G2345</itemno>
   <description>A Gadget</description>
</orderitem>

每則 orderitem 都被補上了訂單日期、訂單編號與客戶 ID:

加入客戶 ID 與訂單日期讓訊息自我完備,消費端因此不必跨訊息保存脈絡——這在訊息要交給無狀態伺服器處理時很重要。加入 ordernumber 則是為了稍後重新聚合(見 Aggregator)。

本例假設商品的具體順序與訂單完成無關,因此不必加上項目序號。

class XMLSplitter
{
    protected MessageQueue inQueue;
    protected MessageQueue outQueue;

    public XMLSplitter(MessageQueue inQueue, MessageQueue outQueue)
    {
        this.inQueue = inQueue;
        this.outQueue = outQueue;

        inQueue.ReceiveCompleted += new ReceiveCompletedEventHandler(OnMessage);
        inQueue.BeginReceive();

        outQueue.Formatter = new ActiveXMessageFormatter();
    }

    protected void OnMessage(Object source, ReceiveCompletedEventArgs asyncResult)
    {
        MessageQueue mq = (MessageQueue)source;
        mq.Formatter = new ActiveXMessageFormatter();
        Message message = mq.EndReceive(asyncResult.AsyncResult);

        XmlDocument doc = new XmlDocument();
        doc.LoadXml((String)message.Body);

        XmlNodeList nodeList;
        XmlElement root = doc.DocumentElement;

        XmlNode date = root.SelectSingleNode("date");
        XmlNode ordernumber = root.SelectSingleNode("ordernumber");
        XmlNode id = root.SelectSingleNode("customer/id");
        XmlElement customerid = doc.CreateElement("customerid");
        customerid.InnerText = id.InnerXml;
        nodeList = root.SelectNodes("/order/orderitems/item");

        foreach (XmlNode item in nodeList)
        {
            XmlDocument orderItemDoc = new XmlDocument();
            orderItemDoc.LoadXml("<orderitem/>");
            XmlElement orderItem = orderItemDoc.DocumentElement;

            orderItem.AppendChild(orderItemDoc.ImportNode(date, true));
            orderItem.AppendChild(orderItemDoc.ImportNode(ordernumber, true));
            orderItem.AppendChild(orderItemDoc.ImportNode(customerid, true));

            for (int i=0; i < item.ChildNodes.Count; i++)
            {
                orderItem.AppendChild(orderItemDoc.ImportNode(item.ChildNodes[i], true));
            }

            outQueue.Send(orderItem.OuterXml);
        }

        mq.BeginReceive();
    }
}

程式碼多數在處理 XML。XMLSplitter 使用與其他路由範例相同的 Event-Driven Consumer 結構:先把訊息內文轉成 XML 文件、取出相關值,再用 XPath 運算式 /order/orderitems/item 走訪每個 <item> 子元素,為每個項目組出一份新 XML 文件。

範例:用 C# 與 XSL 分割 XML 訂單文件

與其手動操作 XML 節點,也可以用一份 XSL 文件把進站 XML 轉成想要的格式,再從轉換後的文件產生輸出訊息。

class XSLSplitter
{
    protected MessageQueue inQueue;
    protected MessageQueue outQueue;

    protected String styleSheet = "..\\..\\Order2OrderItem.xsl";
    protected XslTransform xslt;

    public XSLSplitter(MessageQueue inQueue, MessageQueue outQueue)
    {
        this.inQueue = inQueue;
        this.outQueue = outQueue;

        xslt = new XslTransform();
        xslt.Load(styleSheet, null);

        outQueue.Formatter = new ActiveXMessageFormatter();

        inQueue.ReceiveCompleted += new ReceiveCompletedEventHandler(OnMessage);
        inQueue.BeginReceive();
    }

    protected void OnMessage(Object source, ReceiveCompletedEventArgs asyncResult)
    {
        MessageQueue mq = (MessageQueue)source;
        mq.Formatter = new ActiveXMessageFormatter();
        Message message = mq.EndReceive(asyncResult.AsyncResult);

        try
        {
            XPathDocument doc = new XPathDocument(new StringReader((String)message.Body));

            XmlReader reader = xslt.Transform(doc, null, new XmlUrlResolver());

            XmlDocument allItems = new XmlDocument();
            allItems.Load(reader);

            XmlNodeList nodeList = allItems.DocumentElement.GetElementsByTagName("orderitem");
            foreach (XmlNode orderItem in nodeList)
            {
                outQueue.Send(orderItem.OuterXml);
            }
        }
        catch (Exception e) { Console.WriteLine(e.ToString()); }
        mq.BeginReceive();
    }
}

XSL 文件從獨立檔案讀入,方便編輯與測試,也讓我們不必重新編譯就能改變 Splitter 的行為

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

    <xsl:template match="/order">
        <orderitems>
            <xsl:apply-templates select="orderitems/item"/>
        </orderitems>
    </xsl:template>

    <xsl:template match="item">
        <orderitem>
            <date>
                <xsl:value-of select="parent::node()/parent::node()/date"/>
            </date>
            <ordernumber>
                <xsl:value-of select="parent::node()/parent::node()/ordernumber"/>
            </ordernumber>
            <customerid>
                <xsl:value-of select="parent::node()/parent::node()/customer/id"/>
            </customerid>
            <xsl:apply-templates select="*"/>
        </orderitem>
    </xsl:template>

    <xsl:template match="*">
        <xsl:copy>
            <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

這份轉換尋找 order 元素,找到後為輸出文件建立新的根元素,接著處理 orderitems 內的所有 item;對每個 item 套用一個新 template,從(項目的祖父)order 元素複製 dateordernumbercustomerid,再附上項目本身的所有元素。

順帶做的效能比較#

作者做了一個快速、非嚴謹的測試:把 5000 則訂單訊息灌進輸入佇列,啟動 Splitter,測量 10,000 則項目訊息抵達輸出佇列所需的時間(全部在單一機器、單一程式、本地佇列中執行):

  • 用 DOM 取元素的 XMLSplitter7 秒
  • 基於 XSL 的 Splitter:5.3 秒
  • 基準線(一個消費一則、發布兩則的假處理器):略低於 2 秒

看來 XSL 操作比「手動搬元素」略有效率——扣掉基準線後,XSL 大約快 35%。兩個程式當然都還能為極致效能調校,但把它們並排跑一次仍然很有意思。