博客 / 詳情

返回

拒絕 if-else:利用 Jackson 多態註解 (@JsonTypeInfo) 重構複雜的 IM 消息處理邏輯

引言:被“上帝類”支配的恐懼

在後端開發中,對接第三方 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-elseswitch-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: 在父級模型中“注入靈魂”

這是最關鍵的一步。我們需要在包含 messageTypepayload 的父類 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,我們做到了:

  1. 類型安全:再也不用擔心在圖片消息裏點出 text 字段,因為 ImagePayload 里根本沒有 text
  2. 符合開閉原則:新增一種消息類型,只需新建一個 Payload 子類並註冊註解,無需修改核心業務邏輯。
  3. 邏輯內聚:每種消息的特有邏輯(如生成快照、格式化)封裝在各自的類中,不再散落在 Service 的 if-else 裏。

好的代碼應該是描述“它是什麼”(聲明式),而不是描述“怎麼做”(命令式)。

本文由mdnice多平台發佈

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.