拒绝 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多平台发布

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容