引言:被“上帝類”支配的恐懼
在後端開發中,對接第三方 IM 系統(如微信、企業微信、或 RPA 機器人)的回調接口往往是一場噩夢。
通常,上游為了省事,會丟給你一個聚合的 JSON。不管消息是文本、圖片、還是系統通知,數據都塞在一個通用的 payload 對象裏,全靠外層的 messageType 來區分。
為了接收這個 JSON,我們往往會被迫寫出一個 "上帝類" (God Class):
// 典型的“大雜燴”定義,充滿了不同業務的字段
public class Payload {
private String text; // 文本消息用
private String imageUrl; // 圖片消息用
private Integer duration; // 語音消息用
private String title; // 鏈接消息用
private String cardWxid; // 名片消息用
// ... 此處省略 50 個字段 ...
}
隨之而來的,是業務代碼中漫天飛舞的 if-else 或 switch-case。比如,我們需要生成會話列表的“最新消息摘要”:
// 典型的“麪條代碼”
public String getMessageDigest(ChatMessage msg) {
Integer type = msg.getMessageType();
if (type == 7) {
// 甚至還要判空,因為你不知道 Payload 裏哪個字段有值
return msg.getPayload().getText();
} else if (type == 6) {
return "[圖片]";
} else if (type == 2) {
return "[語音]";
} else if (type == 10000) {
return "[系統消息]";
}
// ... 無休止的 else if ...
return "[未知消息]";
}
這種代碼不僅難看,而且極不安全(拿到 Payload 對象時,你根本不知道哪些字段有效),且違反開閉原則(每新增一種消息類型,都要修改主類和所有相關的 if-else)。
今天,我們來聊聊如何利用 Jackson 的 多態反序列化(Polymorphic Deserialization),徹底重構這段邏輯。
重構核心:策略模式 + Jackson 註解
我們的目標是:讓 Jackson 在解析 JSON 時,自動看一眼 messageType 的值,然後決定把 payload 轉成哪個具體的子類對象。
難點:兄弟字段的關聯
這裏有一個技術難點。通常的多態 JSON,類型字段是在對象內部的。但我們的 IM 回調 JSON 長這樣(類型字段在對象外面,是兄弟關係):
{
"messageType": 7,
"payload": { "text": "你好,世界" }
}
這時候,Jackson 的默認配置就不靈了,我們需要用到一個高級特性:JsonTypeInfo.As.EXTERNAL_PROPERTY。
實戰步驟
Step 1: 定義抽象父類與行為
我們定義一個基類 PayloadBase。精彩的地方來了:我們可以在這裏定義抽象方法,利用多態特性將業務邏輯(如生成摘要)下沉到子類中。
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
/**
* 消息內容基類
* 所有具體消息類型都要繼承此類
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract class PayloadBase {
/**
* 核心抽象方法:獲取消息摘要
* 此時,生成摘要不再是 Service 層的邏輯,而是數據對象自己的能力
*/
public abstract String getDigest();
}
Step 2: 拆分具體的子類
現在,我們可以把那個巨大的 Payload 拆分成乾淨清爽的小類。每個類只需要關注自己的字段和邏輯。
1. 文本消息 Payload (TextPayload)
@Data
@EqualsAndHashCode(callSuper = true)
public class TextPayload extends PayloadBase {
// 只有文本消息才有的字段
private String text;
private String[] mention;
@Override
public String getDigest() {
// 文本消息的摘要就是內容本身
return text != null ? text : "";
}
}
2. 圖片消息 Payload (ImagePayload)
@Data
@EqualsAndHashCode(callSuper = true)
public class ImagePayload extends PayloadBase {
// 只有圖片消息才有的字段
private String imageUrl;
private int size;
// 原圖數據 (只有圖片有,文本消息裏就不該出現這個字段)
private Object artwork;
@Override
public String getDigest() {
// 圖片消息的摘要是固定的
return "[圖片]";
}
}
Step 3: 在父級模型中“注入靈魂”
這是最關鍵的一步。我們需要在包含 messageType 和 payload 的父類 DTO 中配置映射關係。
@Data
public class ChatMessageDto {
/**
* 類型標識字段 (必須存在)
*/
private Integer messageType;
/**
* 核心配置:
* 1. use = Id.NAME: 使用名稱(7, 6, 1)來匹配
* 2. include = As.EXTERNAL_PROPERTY: 關鍵!告訴 Jackson 類型信息在 payload 的"外面"
* 3. property = "messageType": 指明那個外部兄弟字段的名字叫 messageType
*/
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "messageType",
visible = true // 設為 true,讓上面的 messageType 字段本身也能被讀取
)
@JsonSubTypes({
// 映射表:當 messageType = "7" 時 -> 實例化 TextPayload
@JsonSubTypes.Type(value = TextPayload.class, name = "7"),
// 當 messageType = "6" 時 -> 實例化 ImagePayload
@JsonSubTypes.Type(value = ImagePayload.class, name = "6"),
// 當 messageType = "2" 時 -> 實例化 VoicePayload
@JsonSubTypes.Type(value = VoicePayload.class, name = "2"),
// 入羣邀請
@JsonSubTypes.Type(value = InvitePayload.class, name = "9999")
})
private PayloadBase payload;
// ... 其他字段 ...
}
重構後的收益:業務邏輯的“自動化”
回到文章開頭的痛點。重構後,當我們需要獲取“最新消息快照”來更新會話列表時,代碼變成了這樣:
❌ 重構前 (Service 層代碼):
String digest;
switch (dto.getMessageType()) {
case 7: digest = dto.getPayload().getText(); break;
case 6: digest = "[圖片]"; break;
// ... 每次加新消息類型,這裏都要改,容易漏 ...
default: digest = "[未知消息]";
}
session.setLastMessageDigest(digest);
✅ 重構後 (Service 層代碼):
// 沒有任何 if-else!
// 具體的邏輯下沉到了各個子類中,多態機制會自動調用正確的方法
// 即使未來新增了“視頻號消息”,這一行代碼都不用動!
String digest = dto.getPayload().getDigest();
session.setLastMessageDigest(digest);
總結
後端開發不僅僅是 CRUD,合理利用庫的高級特性可以極大地提升代碼的可讀性和健壯性。
通過引入 @JsonTypeInfo,我們做到了:
- 類型安全:再也不用擔心在圖片消息裏點出
text字段,因為ImagePayload里根本沒有text。 - 符合開閉原則:新增一種消息類型,只需新建一個
Payload子類並註冊註解,無需修改核心業務邏輯。 - 邏輯內聚:每種消息的特有邏輯(如生成快照、格式化)封裝在各自的類中,不再散落在 Service 的
if-else裏。
好的代碼應該是描述“它是什麼”(聲明式),而不是描述“怎麼做”(命令式)。
本文由mdnice多平台發佈