引言:被“上帝类”支配的恐惧
在后端开发中,对接第三方 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多平台发布