脈絡:許多穿過整合方案的訊息由多個元素組成。例如客戶下的一張訂單不只一項商品,而如同 Content-Based Router 所述,每一項商品可能得由不同的庫存系統處理。因此我們需要一種作法:處理一整張訂單,卻個別對待其中的每一項商品。
解法必須夠通用#
這個路由問題的解法必須通用到能應付數量與型別都會變化的元素。訂單可以有任意多項商品,所以不能假設固定的項數;對訊息含有哪些型別的商品也不該做太多假設——若 Widget & Gadget 公司明天開始賣書,我們希望對整體方案的衝擊降到最低。
幾條走不通的路#
用 Publish-Subscribe Channel 把整張訂單送給每個系統,讓它們各自挑走能處理的項目#
這與 Content-Based Router 中討論過的缺點相同:很難避免個別商品漏出或重複出貨。
效率考量#
解法也該有效率地使用網路資源。把完整的訂單訊息送給每個「只處理其中一部分」的系統,會造成額外的訊息流量,尤其目的地數量增加時。
把訊息拆成「每個庫存系統一則」#
我們可以把原始訊息拆成與庫存系統數量相同的訊息,每則只含該系統能處理的品項。這種作法類似 Content-Based Router,只是我們先分割、再路由個別訊息。
這樣是有效率,但把方案綁死在「特定商品類型與其對應目的地」的知識上。要改路由規則怎麼辦?我們就得去改那個更複雜的「itemrouter」元件。
我們採用 Pipes and Filters 架構,本來就是為了把處理拆成定義良好、可組合的元件,而不是把多個功能揉成一團——這裡也該善用這個架構。
解法#
Splitter 消費一則含有重複元素清單的訊息,為原始訊息中的每個元素(或元素的子集)各發布一則訊息。
讓子訊息自我完備#
許多情況下,我們希望在每則結果訊息中重複某些共同元素。這些額外元素讓子訊息自我完備,因而能被無狀態地處理,也讓稍後調解相關子訊息成為可能。
例如每則訂單項目訊息都該含有一份訂單編號的副本,好把項目正確地關聯回訂單、以及訂單所關聯的其他實體(如下單的客戶)。
迭代式分割器#
許多企業整合系統把訊息資料存成樹狀結構。樹狀結構的美妙之處在於它是遞迴的:某節點底下的每個子節點,都是另一棵子樹的根。
這讓我們能取出訊息樹的某些片段,把它們當作獨立的訊息樹繼續處理。使用訊息樹時,Splitter 可以很容易地被設定成走訪指定節點底下的所有子節點,並為每個子節點送出一則訊息。
這樣的 Splitter 實作完全通用——它對子元素的數量與型別不做任何假設。許多商業 EAI 工具以 Iterator 或 Sequencer 之名提供這類功能;為了避免廠商詞彙造成混淆,本書稱這種風格為 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 元素複製 date、ordernumber 與 customerid,再附上項目本身的所有元素。
順帶做的效能比較#
作者做了一個快速、非嚴謹的測試:把 5000 則訂單訊息灌進輸入佇列,啟動 Splitter,測量 10,000 則項目訊息抵達輸出佇列所需的時間(全部在單一機器、單一程式、本地佇列中執行):
- 用 DOM 取元素的
XMLSplitter:7 秒 - 基於 XSL 的 Splitter:5.3 秒
- 基準線(一個消費一則、發布兩則的假處理器):略低於 2 秒
看來 XSL 操作比「手動搬元素」略有效率——扣掉基準線後,XSL 大約快 35%。兩個程式當然都還能為極致效能調校,但把它們並排跑一次仍然很有意思。