消息推送接口設計(內含源碼)

我是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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章