🎯 什麼情境該想到我
當你「有一個一般的服務(普通方法),想把它接到訊息通道,讓它既能被訊息觸發、也能照常被程式直接呼叫」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:在訊息通道與一個服務之間搭一座橋,把到達的訊息轉成對服務的呼叫,使服務可被訊息驅動,同時仍能支援非訊息(直接)呼叫。
做法 / 結構要點:
- Service Activator 監聽通道、收訊息。
- 把訊息內容解析成服務方法的參數並呼叫服務。
- 若需回覆,把服務結果包成訊息送回。服務本身不需知道自己被訊息呼叫。
★ 核心價值:業務邏輯只寫一次,入口可以有很多個
HTTP handler ──┐
gRPC handler ──┤
訊息消費者 ──┼──▶ ★ 同一個 Service.Do(ctx, cmd) ──▶ 領域邏輯
CLI 指令 ──┤
排程器 ──┘
// ★ 業務層:不知道自己被誰呼叫
package fulfilment
type Service struct{ repo Repo; pub Publisher }
func (s *Service) Dispatch(ctx context.Context, cmd DispatchCommand) (Result, error) { … }
// ★ 各個「啟動器」:只負責把外界的形狀轉成 cmd
package httpapi
func (h *Handler) Dispatch(w http.ResponseWriter, r *http.Request) {
cmd, err := decodeCommand(r) // ★ HTTP 特有
if err != nil { http.Error(w, …); return }
res, err := h.svc.Dispatch(r.Context(), cmd)
writeJSON(w, res, err) // ★ HTTP 特有
}
package msgapi
func (a *Activator) Handle(ctx context.Context, msg Message) error {
cmd, err := toCommand(msg) // ★ 訊息特有
if err != nil { return fmt.Errorf("%w: %v", ErrInvalidMessage, err) }
_, err = a.svc.Dispatch(ctx, cmd)
return err // ★ 由消費迴圈決定 ack/nack
}★★ 這個模式最大的價值:測試策略
★ 業務邏輯的測試:★★ 直接呼叫 svc.Dispatch(ctx, cmd) —— 不需要 HTTP、不需要 broker
★ 啟動器的測試: 只測「轉換」與「錯誤對應」,量少且簡單
→ ★ 這讓「業務邏輯的測試」數量多、跑得快;「整合測試」數量少、只驗管線
(見 [[測試金字塔]])
★★ 反過來說,沒有這個模式的症狀很明顯:
業務邏輯寫在 HTTP handler 裡 → 要測一條業務規則就得起一個 HTTP server、
組一個 request;而同一條規則從訊息進來時,你只能複製一份程式碼。
★★ 但兩個入口的語義差異必須明確處理
| HTTP/gRPC | 訊息 | |
|---|---|---|
| 重複 | ★ 呼叫端重試才會重複 | ★★ 一定會重複(至少一次) |
| 錯誤處理 | 回 4xx/5xx 給呼叫端 | ★ 決定 ack/nack/DLQ |
| 超時 | 呼叫端會放棄 | ★ 沒人在等,超時是自己設的 |
| 併發 | 由入站流量決定 | ★ 由 prefetch/worker 數決定 |
| 授權 | ★ 有使用者身分(JWT/session) | ★★ 沒有使用者身分——只有訊息 |
★★ 最容易出錯的是「授權」這一列:
HTTP 路徑上有「這個使用者能不能做這件事」的檢查,
而訊息路徑上沒有 → ★ 如果檢查寫在 HTTP handler 裡,訊息路徑就繞過了它。
★ 對策:授權檢查要在業務層(cmd裡帶 actor),不在 handler 裡。
★ 第二容易出錯的是冪等:
// ★ 冪等該放哪?
// ① 放業務層 → ★ 兩個入口都受保護,但 HTTP 也要傳 idempotency key
// ② 放訊息啟動器 → ★ HTTP 路徑沒有保護
// ★★ 判準:這個操作從 HTTP 進來也可能被重試嗎?會 → 放業務層★ 什麼時候不需要這個模式
- ★ 只有一個入口,而且不會增加 → 直接寫在 handler 裡更簡單。
⚠️ 但「之後要加訊息入口」的機率不低,而事後拆很貴。 - ★ 兩個入口的語義差異太大(例如訊息路徑是批次、HTTP 是單筆)
→ 硬共用會讓cmd長出IsBatch bool這種欄位(見 移除旗標參數)。
★ 兄弟辨析
| 差別 | |
|---|---|
| 訊息閘道 | ★ 封裝「往外發」與 broker API;服務啟動器是「從外進來」 |
| 通道配接器 | ★ 在應用程式外面;服務啟動器在裡面 |
| 訊息對應器 | 服務啟動器內部用它做型別轉換 |
| 服務層 | ★ PoEAA 的同一想法(業務邏輯的統一入口) |
| 遠端外觀 | 粗粒度的遠端介面 |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
服務要能同時服務訊息與非訊息呼叫,本體須維持與傳輸無關。
-
錯誤 / 回覆處理要在 Activator 一層做好。
-
常與訊息閘道對稱:一個負責送出、一個負責接入觸發。
-
★★ 授權檢查要在業務層(
cmd帶 actor),不要寫在 HTTP handler 裡——否則訊息路徑會繞過它。這是這個模式最危險的漏洞。 -
★★ 冪等的位置取決於「HTTP 路徑會不會也被重試」。會 → 放業務層,且 HTTP 也要傳 idempotency key。
-
★ 兩個入口的語義差異必須明確列出並處理:重複、錯誤處理、超時、併發、授權。
-
★ 業務邏輯寫在 handler 裡的症狀:要測一條規則得起 HTTP server;同一規則從訊息進來只能複製程式碼。
-
★ 兩個入口語義差異太大時不要硬共用。
cmd長出IsBatch bool就是訊號。 -
錯誤型別要能被兩邊翻譯:業務層回
ErrNotFound,HTTP 譯成 404、訊息路徑譯成「永久錯誤 → DLQ」。 -
只有一個入口時這是過度設計——但事後拆很貴,值得權衡。
🔗 相關工具
- 訊息閘道 Messaging Gateway(同家族,送出端的對稱角色)
- 事件驅動消費者 Event-Driven Consumer(同家族,可用來觸發服務)
- 企業整合模式
- 訊息閘道 —— 往外發的對照
- 通道配接器 —— 在應用程式外面
- 訊息對應器 —— 型別轉換
- 服務層、遠端外觀 —— PoEAA 的同一想法
- 冪等接收者 —— 冪等該放哪一層
- 測試金字塔 —— 這個模式帶來的測試策略