我是3y,一年CRUD
經驗用十年的markdown
程序員👨🏻💻常年被譽爲優質八股文選手
今天要做的就是實現austin-api
和austin-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