设计模式与序列化:策略模式选择序列化方式、适配器兼容格式、装饰器增强序列化

序列化这事儿,说简单也简单,说复杂也复杂。我早年做分布式系统的时候,就吃过序列化的亏——当时项目里用了Java原生的序列化,结果后来要跟C++的服务通信,格式完全不兼容,改得我头皮发麻。从那以后,我就养成了一个习惯:序列化方案一定要设计得灵活,能随时切换。

今天咱们就聊聊,怎么用设计模式把序列化这块做得既灵活又强大。说白了,就是三个模式打配合:策略模式让你能随意切换序列化方式,适配器模式帮你兼容各种奇葩格式,装饰器模式则给序列化过程加点“料”。

核心问题:序列化为什么需要设计模式?

你想想看,一个正经的系统,往往要面对多种序列化需求:

  • 有的场景要JSON,因为前端好解析
  • 有的场景要Protobuf,因为性能要求高
  • 有的场景要XML,因为历史遗留系统只认这个
  • 有的场景要加密压缩后再序列化

如果把这些逻辑都写死在代码里,那改起来可就酸爽了。我见过一个项目,序列化代码里if-else写了七八层,每次加新格式都要改核心逻辑,测试回归一遍,那叫一个痛苦。

核心思路:把“序列化方式”这个变化点抽象出来,用策略模式管理;用适配器模式对接第三方序列化库;用装饰器模式给序列化过程添加额外功能。

一、策略模式:让序列化方式可切换

策略模式在这里的角色很明确——定义一组序列化算法,让它们可以互相替换。客户端只需要知道接口,不需要关心具体实现。

先看接口定义:

// 序列化策略接口
public interface SerializationStrategy {
    <T> byte[] serialize(T obj);
    <T> T deserialize(byte[] data, Class<T> clazz);
}

然后是实现类。比如JSON策略:

public class JsonSerializationStrategy implements SerializationStrategy {
    private final ObjectMapper mapper = new ObjectMapper();
    
    @Override
    public <T> byte[] serialize(T obj) {
        try {
            return mapper.writeValueAsBytes(obj);
        } catch (Exception e) {
            throw new SerializationException("JSON序列化失败", e);
        }
    }
    
    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
        try {
            return mapper.readValue(data, clazz);
        } catch (Exception e) {
            throw new SerializationException("JSON反序列化失败", e);
        }
    }
}

再比如Protobuf策略:

public class ProtobufSerializationStrategy implements SerializationStrategy {
    @Override
    public <T> byte[] serialize(T obj) {
        if (!(obj instanceof MessageLite)) {
            throw new IllegalArgumentException("对象必须实现MessageLite接口");
        }
        return ((MessageLite) obj).toByteArray();
    }
    
    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
        // 通过反射调用parseFrom方法
        try {
            Method method = clazz.getMethod("parseFrom", byte[].class);
            return clazz.cast(method.invoke(null, data));
        } catch (Exception e) {
            throw new SerializationException("Protobuf反序列化失败", e);
        }
    }
}

客户端怎么用?很简单:

public class SerializationContext {
    private SerializationStrategy strategy;
    
    public void setStrategy(SerializationStrategy strategy) {
        this.strategy = strategy;
    }
    
    public byte[] serialize(Object obj) {
        return strategy.serialize(obj);
    }
    
    public <T> T deserialize(byte[] data, Class<T> clazz) {
        return strategy.deserialize(data, clazz);
    }
}

嗯,这里要注意:策略模式的关键在于“运行时切换”。我建议把策略的选择权交给配置文件或注册中心,而不是硬编码在代码里。

二、适配器模式:兼容第三方序列化库

实际项目中,我们经常要对接第三方序列化库。比如Kryo、FST、MessagePack这些,它们的接口跟咱们定义的SerializationStrategy可能不一样。这时候适配器模式就派上用场了。

举个例子,Kryo的用法是这样的:

// Kryo原生API
Kryo kryo = new Kryo();
kryo.register(SomeClass.class);
byte[] data = kryo.toBytes(obj);
SomeClass obj2 = kryo.fromBytes(data);

跟咱们的接口对不上,怎么办?写个适配器:

public class KryoAdapter implements SerializationStrategy {
    private final Kryo kryo;
    private final ThreadLocal<Kryo> kryoThreadLocal;
    
    public KryoAdapter() {
        this.kryo = new Kryo();
        this.kryo.setRegistrationRequired(false);
        // 线程池场景下,Kryo实例不是线程安全的,需要ThreadLocal
        this.kryoThreadLocal = ThreadLocal.withInitial(() -> {
            Kryo instance = new Kryo();
            instance.setRegistrationRequired(false);
            return instance;
        });
    }
    
    @Override
    public <T> byte[] serialize(T obj) {
        Kryo localKryo = kryoThreadLocal.get();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Output output = new Output(baos);
        localKryo.writeClassAndObject(output, obj);
        output.close();
        return baos.toByteArray();
    }
    
    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
        Kryo localKryo = kryoThreadLocal.get();
        Input input = new Input(new ByteArrayInputStream(data));
        Object obj = localKryo.readClassAndObject(input);
        input.close();
        return clazz.cast(obj);
    }
}

适配器模式的好处是:你不需要改第三方库的代码,也不需要改业务代码,中间加一层适配就搞定了。我在项目中遇到过好几次,老系统用的序列化库版本太旧,升级成本高,就是用适配器模式把新旧接口桥接起来的。

三、装饰器模式:给序列化加点“料”

有时候,光序列化还不够。你可能需要:

  • 序列化前先压缩数据
  • 序列化后加密
  • 加个校验和防止数据篡改
  • 记录序列化耗时用于监控

这些功能如果都塞到策略实现里,那策略类会变得臃肿不堪。装饰器模式就是用来解决这个问题的——它让你能动态地给序列化过程添加行为。

先定义一个装饰器基类:

public abstract class SerializationDecorator implements SerializationStrategy {
    protected final SerializationStrategy wrapped;
    
    public SerializationDecorator(SerializationStrategy wrapped) {
        this.wrapped = wrapped;
    }
    
    @Override
    public <T> byte[] serialize(T obj) {
        return wrapped.serialize(obj);
    }
    
    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
        return wrapped.deserialize(data, clazz);
    }
}

然后实现具体的装饰器。比如压缩装饰器:

public class CompressionDecorator extends SerializationDecorator {
    public CompressionDecorator(SerializationStrategy wrapped) {
        super(wrapped);
    }
    
    @Override
    public <T> byte[] serialize(T obj) {
        byte[] data = super.serialize(obj);
        return compress(data);
    }
    
    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
        byte[] decompressed = decompress(data);
        return super.deserialize(decompressed, clazz);
    }
    
    private byte[] compress(byte[] data) {
        // 使用GZIP压缩
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (GZIPOutputStream gzip = new GZIPOutputStream(baos)) {
            gzip.write(data);
        } catch (IOException e) {
            throw new SerializationException("压缩失败", e);
        }
        return baos.toByteArray();
    }
    
    private byte[] decompress(byte[] data) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(data))) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = gzip.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            throw new SerializationException("解压失败", e);
        }
        return baos.toByteArray();
    }
}

再比如加密装饰器:

public class EncryptionDecorator extends SerializationDecorator {
    private final SecretKey key;
    
    public EncryptionDecorator(SerializationStrategy wrapped, SecretKey key) {
        super(wrapped);
        this.key = key;
    }
    
    @Override
    public <T> byte[] serialize(T obj) {
        byte[] data = super.serialize(obj);
        return encrypt(data);
    }
    
    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
        byte[] decrypted = decrypt(data);
        return super.deserialize(decrypted, clazz);
    }
    
    private byte[] encrypt(byte[] data) {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] iv = cipher.getIV();
            byte[] encrypted = cipher.doFinal(data);
            // 把IV拼在密文前面
            ByteBuffer buffer = ByteBuffer.allocate(iv.length + encrypted.length);
            buffer.put(iv);
            buffer.put(encrypted);
            return buffer.array();
        } catch (Exception e) {
            throw new SerializationException("加密失败", e);
        }
    }
    
    private byte[] decrypt(byte[] data) {
        try {
            ByteBuffer buffer = ByteBuffer.wrap(data);
            byte[] iv = new byte[12]; // GCM标准IV长度12字节
            buffer.get(iv);
            byte[] encrypted = new byte[buffer.remaining()];
            buffer.get(encrypted);
            
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            GCMParameterSpec spec = new GCMParameterSpec(128, iv);
            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            return cipher.doFinal(encrypted);
        } catch (Exception e) {
            throw new SerializationException("解密失败", e);
        }
    }
}

客户端怎么组合使用?就像叠buff一样:

// 基础策略
SerializationStrategy base = new JsonSerializationStrategy();

// 先压缩,再加密
SerializationStrategy compressed = new CompressionDecorator(base);
SerializationStrategy encryptedAndCompressed = new EncryptionDecorator(compressed, secretKey);

// 使用
byte[] data = encryptedAndCompressed.serialize(myObject);
MyObject obj = encryptedAndCompressed.deserialize(data, MyObject.class);

我的经验:装饰器的顺序很重要。一般来说,先压缩再加密效率更高,因为加密后的数据是伪随机的,压缩效果很差。反过来,先加密再压缩,压缩率会大打折扣。这个坑我曾经踩过,当时线上数据量大了好几倍才发现问题。

四、整体架构:三个模式如何协同

这三个模式不是孤立的,它们可以组合成一个完整的序列化框架。我画了张图,帮你理清关系:

序列化框架架构:三种设计模式协同 客户端代码 SerializationContext «接口» SerializationStrategy JSON策略 Protobuf策略 Kryo适配器 装饰器层:压缩 | 加密 | 校验 | 监控 调用方 策略接口 具体策略/适配器 装饰器

从图上可以看得很清楚:客户端通过SerializationContext与策略接口交互,策略接口有多个实现(包括适配器包装的第三方库),而装饰器则像洋葱一样包裹在策略外面,层层叠加功能。

五、实战中的避坑指南

讲完了理论,聊聊我在实际项目中踩过的坑:

我曾经犯过的错:

  • 线程安全问题:Kryo、Protobuf这些库的实例往往不是线程安全的。我一开始没注意,在并发场景下直接复用同一个Kryo实例,结果数据错乱,排查了两天才找到原因。解决方案就是用ThreadLocal,每个线程持有一个独立实例。
  • 版本兼容问题:序列化后的数据可能会长期存储,比如消息队列里的积压消息。如果序列化策略升级了,老数据反序列化就会失败。我建议在序列化数据头部加一个版本号字段,反序列化时根据版本号选择对应的策略。
  • 装饰器顺序问题:前面提过,先压缩再加密,别搞反了。另外,如果加了校验和装饰器,一定要放在最外层,这样数据在传输过程中被篡改了能第一时间发现。

六、总结

说白了,设计模式在序列化这个场景里,解决的就是“变化”和“扩展”的问题。策略模式让序列化方式可切换,适配器模式让第三方库可接入,装饰器模式让功能可叠加。三个模式各司其职,组合起来就是一个灵活、可扩展的序列化框架。

我个人习惯在设计序列化方案时,先想清楚哪些是稳定的(接口),哪些是变化的(具体策略),哪些是附加的(装饰器功能)。想清楚了再动手,代码写出来就清爽多了。

最后送大家一句话:好的架构不是设计出来的,是演化出来的。设计模式给了你演化的工具,但具体怎么用,还得看你的业务场景。多实践,多总结,慢慢就有感觉了。

核心要点回顾:

  • 策略模式:定义序列化算法族,支持运行时切换
  • 适配器模式:桥接第三方序列化库,不改原有代码
  • 装饰器模式:动态添加压缩、加密等附加功能
  • 注意线程安全、版本兼容、装饰器顺序

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