脈絡:應用程式透過訊息傳遞存取另一個系統。
直接對著訊息 API 寫程式的四個痛點#
多數自製應用程式透過廠商提供的 API 存取訊息基礎設施。這些 API 風味各異,但大致都暴露類似的功能——「開通道」「建訊息」「送訊息」。
- 意圖不明——這類 API 讓應用程式能在任何通道上送任何訊息資料,卻常常看不出送這則訊息的意圖是什麼。
- 非同步讓程式碼變複雜——與其呼叫一個回傳數值信用分數的
GetCreditScore方法,應用程式得送出請求訊息,然後預期回覆訊息稍後才到(見 Request-Reply)。開發者往往偏好同步函式的簡單語意,而不是去應付進站訊息事件。 - 鬆散耦合的代價——鬆散耦合通常靠 XML 文件或其他非強型別的資料結構達成。
對著這類結構寫程式既繁瑣又容易出錯——沒有編譯期支援來偵測拼錯的欄位名稱或不符的資料型別。我們是用應用程式的開發工夫,換來資料格式上的彈性。
- 一個邏輯功能可能需要多則訊息——例如「取得客戶資訊」實際上可能需要三則訊息:取地址、取訂單歷史、取個人資訊,而且各由不同系統處理。
我們不想讓應用程式碼被「送收三則訊息」的邏輯弄髒。可以用一個 Scatter-Gather 承擔部分負擔(收一則訊息、送出三則、再聚合回一則回覆),但我們未必總有把這個功能加進中介軟體的餘裕。
解法#
Messaging Gateway 把訊息專屬程式碼封裝起來,與應用程式其餘部分分離。於是只有 gateway 知道訊息系統的存在,應用程式其餘部分並不知道。
它對應用程式暴露的是業務功能:與其要求應用程式去設定
Message.MessageReadPropertyFilter.AppSpecific這類屬性,gateway 暴露的是GetCreditScore這種接受強型別參數的方法,就跟其他方法一樣。Messaging Gateway 是更一般的 Gateway 模式 [EAA] 在訊息領域的特化版。
因為應用程式甚至不知道自己在用訊息系統,我們可以把 gateway 換成用其他整合技術(遠端程序呼叫、Web services)的實作。

圖 10-1:Messaging Gateway 解法示意

圖 10-2:Gateway 消除應用程式與訊息系統之間的直接相依
兩種實作方式#
許多 Messaging Gateway 會送訊息給另一個元件並期待回覆(Request-Reply),實作方式有兩種。
阻塞式(同步)Messaging Gateway#
送出訊息後等待回覆抵達才把控制權還給應用程式;收到回覆後處理它並把結果回傳。
它把訊息互動的非同步本質封裝起來,對應用邏輯暴露一個常規的同步方法:
int GetCreditScore(string SSN);應用程式因此完全察覺不到通訊中的任何非同步性,寫起來非常簡單。
但這也可能導致效能低落——應用程式最後大部分時間都在乾等回覆訊息,而那段時間它其實可以做別的事。
事件驅動式(非同步)Messaging Gateway#
它把訊息層的非同步本質暴露給應用程式:應用程式發出領域專屬請求時一併提供一個領域專屬的回呼,控制權立刻返回;回覆訊息抵達時,gateway 處理它並喚起回呼。
以 C# 的 delegate 為例:
delegate void OnCreditReplyEvent(int CreditScore);
void RequestCreditScore(string SSN, OnCreditReplyEvent OnCreditResponse);請注意:儘管介面是事件驅動的,它對特定訊息技術毫無相依。
另一種選擇是讓應用程式定期輪詢結果是否抵達。這讓上層介面保持簡單卻不引入阻塞,本質上就是 Half-sync/Half-async 模式 [POSA2]——用緩衝區存放進站訊息,讓應用程式在方便時輪詢。
事件驅動 gateway 的難處:維護狀態#
事件驅動 gateway 的挑戰之一是:它要求應用程式在「請求方法」與「回呼事件」之間維護狀態(阻塞式作法中,呼叫堆疊替我們處理了這件事)。
當 gateway 把回呼喚進應用邏輯時,應用程式必須能把回覆與稍早發出的請求關聯起來,才能繼續正確的執行緒。
delegate void OnCreditReplyEvent(int CreditScore, Object ACT);
void RequestCreditScore(string SSN, OnCreditReplyEvent OnCreditResponse, Object ACT);支援 ACT 對應用程式非常方便,但它引入了記憶體洩漏的危險——若 gateway 持有某個物件的參考,而預期的回覆訊息卻永遠沒來。
串接 gateway#
- 較低層的 gateway 只抽象掉訊息系統的語法,但保持通用的訊息語意(例如
SendMessage)。當企業更換訊息技術時(例如從 MSMQ 換成 Web Services),它能替應用程式其餘部分擋下衝擊。- 再用另一個 gateway 把這個基本 gateway 包起來,把通用訊息 API 轉譯成狹窄的領域專屬 API(例如
GetCreditScore)。

圖 10-3:串接的 Gateway 提供不同層次的抽象
處理訊息例外#
除了讓應用程式好寫之外,Messaging Gateway 的用意也在消除應用程式碼對特定訊息技術的相依。把訊息專屬方法呼叫包在介面後面很容易做到,但多數訊息層會拋出訊息專屬的例外(例如 JMS 的
InvalidDestinationException)。
若真的想讓應用程式碼獨立於訊息函式庫,gateway 就必須攔截任何訊息專屬例外,改拋出應用程式專屬(或通用)的例外。這段程式碼寫起來有點繁瑣,但當我們必須抽換底層實作(例如從 JMS 換到 Web services)時,它非常有幫助。
自動產生 gateway#
許多情況下,gateway 程式碼可以從外部資源暴露的中介資料自動產生,這在 Web services 世界中很常見:幾乎每個廠商或開源平台都提供
wsdl2java這類工具,連上外部 Web service 暴露的 WSDL,產生把那些討厭的 SOAP 東西封裝起來、只暴露簡單函式呼叫的類別。作者也做過類似的工具:從 TIBCO repository 讀取訊息 schema 定義,產生模仿該 schema 的 Java 原始碼,讓應用開發者不必學 TIBCO API 就能送出型別正確的 TIBCO ActiveEnterprise 訊息。
用 gateway 做測試#
「假」實作扮演 Service Stub [EAA],讓我們能在完全不相依於訊息的情況下測試應用程式。
Service Stub 對「除錯使用事件驅動 gateway 的應用程式」也很有用:一個簡單的事件驅動 gateway 測試 stub,可以直接在請求方法裡就喚起回呼(或 delegate),實際上在同一條執行緒中完成請求與回應處理——這能大幅簡化逐步除錯。

圖 10-4:把 Gateway 當作測試工具
範例:MSMQ 中的非同步貸款仲介 Gateway
這是組合式訊息間奏中 Loan Broker 範例的一部分:
public delegate void OnCreditReplyEvent(CreditBureauReply creditReply, Object ACT);
internal struct CreditRequestProcess
{
public int CorrelationID;
public Object ACT;
public OnCreditReplyEvent callback;
}
internal class CreditBureauGateway
{
protected IMessageSender creditRequestQueue;
protected IMessageReceiver creditReplyQueue;
protected IDictionary activeProcesses = (IDictionary)(new Hashtable());
protected Random random = new Random();
public void Listen()
{
creditReplyQueue.Begin();
}
public void GetCreditScore(CreditBureauRequest quoteRequest,
OnCreditReplyEvent OnCreditResponse, Object ACT)
{
Message requestMessage = new Message(quoteRequest);
requestMessage.ResponseQueue = creditReplyQueue.GetQueue();
requestMessage.AppSpecific = random.Next();
CreditRequestProcess processInstance = new CreditRequestProcess();
processInstance.ACT = ACT;
processInstance.callback = OnCreditResponse;
processInstance.CorrelationID = requestMessage.AppSpecific;
creditRequestQueue.Send(requestMessage);
activeProcesses.Add(processInstance.CorrelationID, processInstance);
}
private void OnCreditResponse(Message msg)
{
msg.Formatter = GetFormatter();
CreditBureauReply replyStruct;
try
{
if (msg.Body is CreditBureauReply)
{
replyStruct = (CreditBureauReply)msg.Body;
int CorrelationID = msg.AppSpecific;
if (activeProcesses.Contains(CorrelationID))
{
CreditRequestProcess processInstance =
(CreditRequestProcess)(activeProcesses[CorrelationID]);
processInstance.callback(replyStruct, processInstance.ACT);
activeProcesses.Remove(CorrelationID);
}
else { Console.WriteLine("Incoming credit response does not match any request"); }
}
else
{ Console.WriteLine("Illegal reply."); }
}
catch (Exception e)
{
Console.WriteLine("Exception: {0}", e.ToString());
}
}
}注意 public 方法
GetCreditScore與 public delegateOnCreditReplyEvent完全沒有提到訊息。這個實作讓呼叫端能傳入任意物件參考作為 Asynchronous Completion Token:
CreditBureauGateway把它存進一個以請求訊息的 Correlation Identifier 為索引的字典;回覆訊息抵達時,就能取回與該出站請求關聯的資料——呼叫端完全不必操心訊息是怎麼被關聯起來的。 >