重构与序列化:重构对象序列化/反序列化逻辑

序列化这事儿,说白了就是把内存里的对象变成一串字节,方便存起来或者传出去。反序列化就是反过来。听起来简单,对吧?但我在项目中见过太多因为序列化逻辑混乱导致的线上事故了。有一次,一个同事改了对象的字段名,忘了改序列化配置,结果老版本的数据反序列化回来全是 null,用户数据直接丢了半年的。嗯,从那以后,我对序列化代码的重构就格外上心。

为什么序列化逻辑需要重构?

你想想看,一个对象在它的生命周期里,可能要经历多次序列化和反序列化。如果序列化逻辑散落在各个地方,比如有的地方用 JSON,有的地方用二进制,有的地方自己拼字符串……那维护起来就是一场噩梦。

我个人习惯把序列化逻辑看作对象的“边界处理”。它不应该和业务逻辑混在一起。重构的目标就是:让序列化逻辑集中、可测试、且与业务解耦

核心原则:序列化是对象的“外部表示”,不是内部行为。重构时,要把“如何序列化”和“序列化什么”分开。

常见的序列化重构场景

我总结了三个最常见的重构场景,你在项目中大概率会遇到:

  • 场景一:序列化逻辑散落在多个类中 —— 每个类自己写 toJson()、fromJson(),重复代码多,改格式时到处改。
  • 场景二:序列化与业务逻辑耦合 —— 比如在业务方法里直接操作 JSON 对象,而不是操作领域对象。
  • 场景三:版本兼容性处理混乱 —— 老版本数据和新版本对象不兼容,反序列化时各种异常。

重构手法:提取序列化器

我最推荐的做法是:把序列化逻辑提取到独立的序列化器(Serializer)中。这样,业务对象只关心自己的状态,序列化器只关心如何转换。

来看一个例子。假设我们有一个用户对象:

// 重构前:序列化逻辑散落在业务代码中
public class User {
    private String name;
    private int age;
    private String email;

    public String toJson() {
        return "{\"name\":\"" + name + "\",\"age\":" + age + "}";
    }

    public static User fromJson(String json) {
        // 手动解析 JSON,容易出错
        ...
    }
}

重构后,我们把序列化逻辑抽出来:

// 重构后:独立的序列化器
public class UserSerializer {
    private final ObjectMapper mapper = new ObjectMapper();

    public String serialize(User user) {
        return mapper.writeValueAsString(user);
    }

    public User deserialize(String json) {
        return mapper.readValue(json, User.class);
    }
}

这样做的好处很明显:

  • 序列化逻辑可以单独测试。
  • 更换序列化框架(比如从 Jackson 换到 Gson)时,只改一个类。
  • 业务代码里不再出现 JSON 操作,干净多了。

处理版本兼容性

我曾经遇到过一个坑:线上跑了三年的老数据,字段名是下划线风格(user_name),新版本改成了驼峰(userName)。反序列化时直接报错。嗯,这里要注意,序列化重构必须考虑向前兼容

我的做法是引入一个版本字段,并在序列化器中做适配:

public class UserSerializerV2 {
    private static final int CURRENT_VERSION = 2;

    public String serialize(User user) {
        // 序列化时带上版本号
        Map<String, Object> data = new HashMap<>();
        data.put("version", CURRENT_VERSION);
        data.put("userName", user.getName());
        data.put("userAge", user.getAge());
        return mapper.writeValueAsString(data);
    }

    public User deserialize(String json) {
        Map<String, Object> data = mapper.readValue(json, Map.class);
        int version = (int) data.getOrDefault("version", 1);

        if (version == 1) {
            // 兼容老版本:字段名是 user_name
            return new User(
                (String) data.get("user_name"),
                (int) data.get("user_age")
            );
        } else if (version == 2) {
            return new User(
                (String) data.get("userName"),
                (int) data.get("userAge")
            );
        } else {
            throw new UnsupportedOperationException("未知版本: " + version);
        }
    }
}

警告:不要在生产环境中直接删除老版本的序列化逻辑。即使你认为没有老数据了,也要保留至少一个版本周期。我曾经因为删得太早,导致回滚时数据全废了。

序列化重构的知识体系

下面这张图是我自己总结的序列化重构核心逻辑,你可以把它当作一个检查清单:

序列化重构 提取序列化器 解耦业务与序列化 单独测试序列化逻辑 版本兼容处理 引入版本字段 适配老版本数据 统一序列化格式 消除散落的序列化代码 统一使用标准序列化框架 目标:可测试、可兼容、可维护

避坑指南

最后,分享几个我踩过的坑:

  • 不要依赖默认序列化行为 —— 比如 Java 的默认序列化,一旦类结构变了,反序列化就炸。我建议始终显式定义序列化逻辑。
  • 警惕循环引用 —— 对象 A 引用 B,B 又引用 A,序列化时会栈溢出。记得用 @JsonIgnore 或自定义引用处理。
  • 测试要覆盖边界 —— 空对象、超大对象、特殊字符(比如 emoji),都要测到。我曾经因为一个 emoji 导致整个反序列化失败,排查了半天。

小技巧:在重构序列化逻辑时,可以先用一个“序列化适配层”过渡。新旧两个序列化器同时存在,通过配置开关切换。等验证没问题了,再删掉旧的。这样风险最小。

序列化重构看起来是技术细节,但影响面很大。你想想看,数据一旦写出去,就很难收回来了。所以,重构序列化逻辑时,慢一点、稳一点,比快更重要


公众号:蓝海资料掘金营,微信deep3321