设计模式与序列化:策略模式选择序列化方式、适配器兼容格式、装饰器增强序列化
序列化这事儿,说简单也简单,说复杂也复杂。我早年做分布式系统的时候,就吃过序列化的亏——当时项目里用了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与策略接口交互,策略接口有多个实现(包括适配器包装的第三方库),而装饰器则像洋葱一样包裹在策略外面,层层叠加功能。
五、实战中的避坑指南
讲完了理论,聊聊我在实际项目中踩过的坑:
我曾经犯过的错:
- 线程安全问题:Kryo、Protobuf这些库的实例往往不是线程安全的。我一开始没注意,在并发场景下直接复用同一个Kryo实例,结果数据错乱,排查了两天才找到原因。解决方案就是用ThreadLocal,每个线程持有一个独立实例。
- 版本兼容问题:序列化后的数据可能会长期存储,比如消息队列里的积压消息。如果序列化策略升级了,老数据反序列化就会失败。我建议在序列化数据头部加一个版本号字段,反序列化时根据版本号选择对应的策略。
- 装饰器顺序问题:前面提过,先压缩再加密,别搞反了。另外,如果加了校验和装饰器,一定要放在最外层,这样数据在传输过程中被篡改了能第一时间发现。
六、总结
说白了,设计模式在序列化这个场景里,解决的就是“变化”和“扩展”的问题。策略模式让序列化方式可切换,适配器模式让第三方库可接入,装饰器模式让功能可叠加。三个模式各司其职,组合起来就是一个灵活、可扩展的序列化框架。
我个人习惯在设计序列化方案时,先想清楚哪些是稳定的(接口),哪些是变化的(具体策略),哪些是附加的(装饰器功能)。想清楚了再动手,代码写出来就清爽多了。
最后送大家一句话:好的架构不是设计出来的,是演化出来的。设计模式给了你演化的工具,但具体怎么用,还得看你的业务场景。多实践,多总结,慢慢就有感觉了。
核心要点回顾:
- 策略模式:定义序列化算法族,支持运行时切换
- 适配器模式:桥接第三方序列化库,不改原有代码
- 装饰器模式:动态添加压缩、加密等附加功能
- 注意线程安全、版本兼容、装饰器顺序