訊息推送介面設計(內含原始碼)

Java3y發表於2022-01-19

我是3y,一年CRUD經驗用十年的markdown程式設計師??‍?常年被譽為優質八股文選手

今天要做的就是實現austin-apiaustin-api-impl模組的部分程式碼,這塊完成了之後模組之間的一整條鏈路就打通咯


austin專案核心功能:傳送訊息

專案出現意義:只要公司內有傳送訊息的需求,都應該要有類似austin的專案,對各類訊息進行統一傳送處理。這有利於對功能的收攏,以及提高業務需求開發的效率

不多BB,開始今天的正題

01、介面設計

austini-api模組下定義傳送訊息的介面,在austin-api-impl下實現具體的邏輯。我的介面實現定義:

public interface SendService {


    /**
     * 單模板單文案傳送介面
     * @param sendRequest
     * @return
     */
    SendResponse send(SendRequest sendRequest);


    /**
     * 單模板多文案傳送介面
     * @param batchSendRequest
     * @return
     */
    SendResponse batchSend(BatchSendRequest batchSendRequest);

}

對外提供的介面,除了需要提供Single介面,最好還提供個Batch介面。因為很有可能業務方是需要一次批量執行的(如果只有Single介面,那就需要多次遠端呼叫,這樣對業務而言就不太合適了)

我所定義的介面引數如下:

public class SendRequest {

    /**
     * 執行業務型別
     */
    private String code;

    /**
     * 訊息模板Id
     */
    private Long messageTemplateId;


    /**
     * 訊息相關的引數
     */
    private MessageParam messageParam;
    
}

通過messageTemplateId可以去資料庫查出整個模板的資訊,而MessageParam則是業務自行傳入的引數(重要的是接收者以及文案的引數資訊),而code則代表著當前請求要執行什麼業務型別的(可基於該code擴充套件,後面會繼續聊到)

02、程式碼實現

從流程可以看到,austin-api接收到請求之後,是把訊息發到MQ

這樣做有什麼好處呢?假設某訊息的服務超時,austin-api如果是直接呼叫下發介面服務,那可能會存在超時風險,拖垮整個介面效能。MQ在這是為了做非同步和解耦,並且在一定程度上抗住業務流量。

對於絕大多數傳送的訊息而言,業務方也不太關心是不是能在介面呼叫時就知道傳送結果,並且某些渠道在傳送的時候也不知道傳送的結果(最後的結果是非同步告知的,比如簡訊和PUSH推送)

基於以上的原因,引入MQ來承載介面的流量以及做非同步,是非常合理的事。

前兩天我在部落格平臺上發了一篇文章《面試官:系統需求多變時如何設計? 》,有網友評論了一把:

面試官:我懂了,回去等通知吧。 …… leader:小王,我們們那個可變系統的重構計劃寫的怎麼樣了? 小王:沒問題了,首先按找我們們的業務區分出責任鏈,然後在每個具體的步驟中部署指令碼,上層再增加一個服務編排的介面統一管理…… leader:聽起來有點意思,今天的候選人怎麼樣? 小王:別提了,嘴上說5年經驗有大型系統設計,連redis都沒用過。這不是快招聘季了嗎,招兩個實習生工具人進來給我打打下手就夠了。 leader:好,把時間節點和里程碑劃分一下,confluence上立項開幹吧。 小王:好嘞。

在這次實現中,我也是用了責任鏈模式,具體完整的程式碼大家就去Gitee拉就好了。很多同學拉完程式碼發現看不懂了,大家可以按照下面的圖去梳理下責任鏈的各個角色。如果實在看不懂,建議翻下我以前寫過的責任鏈文章(已經投稿過兩篇了)

回到程式碼實現吧,這次我實現的業務是:引數前置檢查->引數拼裝->傳送訊息

呀,都畫了這麼多圖了,先點個贊,關注一波先咯。

在這幾個流程中,可能你下次拉程式碼的時候,會看到有“後置檢查”,或者別的什麼的。但不管怎麼樣,加這種邏輯我再也不用在同一個類上寫各種if else啦。只要在某個節點處新增一個Action就完事了。

(注:這是第一版實現,後面肯定會在基礎上新增邏輯或註釋的,其實已經在寫了,但我一般是有個小階段再push程式碼,所以記得star下gitee方便看最新的程式碼)

先來說前置檢查吧,主要就判斷模板ID是否有傳入,訊息引數是否有傳入(對引數的常規檢查,如果有問題,直接break掉鏈路,返回告訴呼叫方有問題)

接著來看引數拼裝,這塊主要就是通過模板ID去查整個模板的內容,然後根據業務入參拼裝出自己的TaskInfo(任務訊息)。

可能有同學會有疑問❓:為什麼不能直接用模板的POJO呢?反而需要拼裝成TaskInfo?

其實還是比較好理解的,模板是作為給使用者去配置該訊息的資訊,這是最最原始的資訊。但是我們傳送的時候是需要做處理的。比如,我要在使用者寫好的URL連結上拼接引數,我要對佔位符進行替換真實的值,我要在模板的基礎上增加業務ID進而追蹤資料 等等等。

說白了,TaskInfo是基於模板的,在模板的基礎上新增了某些平臺性的欄位(businessId),解析出使用者設定的模板而想要傳送的真實內容等等。

在這裡,值得要說明的是msgContent該欄位的說明。在模板中,該欄位我在資料庫註釋所下的定義是(這個欄位存入資料庫一定是JSON格式的):

`msg_content`        varchar(600) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '訊息內容 佔位符用{$var}表示',

不同的渠道的JSON結構還不一樣:

  • 簡訊:{"content":"","url":""}
  • 郵件:{"content":"","subTitle":""}
  • Push:{"content":"","subTitle":"","phoneImgUrl":""}
  • 小程式:{"content":"","pagePath":"" .......}

第一反應,我是想把所有渠道可能用到的欄位都定義在TaskInfo下。後來感覺這樣不太好看,於是我就定義了各種Model(不同的傳送渠道擁有著自己的內容模型)

於是,我在組裝TaskInfo的時候利用反射來進行對映,替換佔位符則藉助的是PropertyPlaceholderHelper

而傳送則很簡單了,我是直接把TaskInfo序列化為JSON,然後讀取的時候再反序列化就好了。

值得注意的是,因為TaskInfo用的是ContentModel來儲存著內容模型,所以我們在序列化JSON的時候需要把"類資訊"寫進去,不然在反序列的時候是拿不到子類的資料的。

03、總結

對於有原始碼的專案,其實我是不太願意每一步講解我寫的程式碼的。因為我認為我本身寫得也沒那麼複雜,也沒有炫技的成分在內。

但自從push了程式碼以後,在群裡提醒各位跟著做專案的小夥伴後,有好幾位向我反饋看不太懂,所以這篇我就單獨拎出來講講。

再回過頭看,其實在austin-api層接收到請求之後,在傳送訊息至MQ之前,在這裡的操作都是非常簡單。其實是可以把通用業務做在這(比如說通用去重的功能),但經我考慮之後,還是不太合適。

austin-api算是一個接入層,到目前為止它只是通過id去資料庫讀取配置,就沒有耗時的操作(這意味著他能承載的併發是極大的)。假設通過ID去資料庫讀取將來存在瓶頸,我們還可以考慮將配置從Redis甚至本地記憶體裡取。

這是由業務可以決定的:一個模板的變更往往並不多,即便快取存在強一致性的問題,但就那點點時間是完全可接受的。

Question :為什麼發個訊息需要MQ?

Answer:傳送訊息實際上是呼叫各個服務提供的API,假設某訊息的服務超時,austin-api如果是直接呼叫服務,那存在超時風險,拖垮整個介面效能。MQ在這是為了做非同步和解耦,並且在一定程度上抗住業務流量。

Question:能簡單說下接入層做了什麼事嗎?

Answer

歡迎關注我的微信公眾號【Java3y】來聊聊Java面試,對線面試官系列持續更新中!

訊息推送介面設計(內含原始碼)

【對線面試官+從零編寫Java專案】 持續高強度更新中!求star!!原創不易!!求三連!!

Gitee連結:https://gitee.com/austin

GitHub連結:https://github.com/austin

相關文章