脈絡:應用程式透過訊息傳遞存取另一個系統。

直接對著訊息 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 delegate OnCreditReplyEvent 完全沒有提到訊息

這個實作讓呼叫端能傳入任意物件參考作為 Asynchronous Completion TokenCreditBureauGateway 把它存進一個以請求訊息的 Correlation Identifier 為索引的字典;回覆訊息抵達時,就能取回與該出站請求關聯的資料——呼叫端完全不必操心訊息是怎麼被關聯起來的。 >