理解序列化与反序列化的基本概念

序列化和反序列化是什么?

在需要将Java对象持久化,例如存储在文件中或通过网络传输时,就需要用到序列化。简单来说:

  • 序列化:将数据结构或对象转化为二进制字节流的过程。
  • 反序列化:将序列化过程中生成的二进制字节流转化回数据结构或对象的过程。

对于Java这种面向对象的编程语言而言,我们序列化的主要是对象(Object),即类(Class)的实例化。而在C++等语言中,struct用于定义数据结构类型,class则对应对象类型。

维基百科对序列化的描述如下:

序列化(serialization)是计算机科学中,将数据结构或对象状态转化为可存储或传输的格式(如文件、缓冲区或通过网络发送),以便在同一或不同计算机环境中恢复原始状态的过程。通过序列化格式重新获取字节的结果,可以产生与原始对象相同语义的副本。对于许多复杂对象,这种重建过程可能非常困难。面向对象中的对象序列化,并不涵盖原始对象所关系的函数。这一过程也称为对象编组(marshalling)。从字节序列提取数据结构的反操作被称为反序列化(也称为解编组、deserialization、unmarshalling)。

综上所述,序列化的主要目的是通过网络传输对象或将对象存储在文件系统、数据库、内存中。

图片https://www.corejavaguru.com/java/serialization/interview\-questions\-1

在开发中序列化与反序列化的应用场景

  1. 在进行网络传输对象之前(例如远程方法调用RPC),对象需要先被序列化,而接收方必须进行反序列化。
  2. 将对象存储到文件时须进行序列化,而从文件中读取对象时则需要反序列化。
  3. 在缓存数据库(如Redis)中存储对象也需要序列化,读取时则需反序列化。

序列化协议在TCP/IP四层模型中的位置

网络通信的双方必须遵循相同的协议。TCP/IP四层模型如下,序列化协议属于哪一层呢?

  1. 应用层
  2. 传输层
  3. 网络层
  4. 网络接口层

图片

如上图所示,OSI七层协议模型中的表示层主要负责将应用层的用户数据转换为二进制流,反向操作则是将二进制流转换回用户数据。这便对应于序列化与反序列化过程。

因此,序列化协议属于TCP/IP协议中应用层的一部分。

常见序列化协议对比

JDK自带的序列化方式通常不被广泛使用,因为其效率低下且部分版本存在安全漏洞。常用的序列化协议包括Hessian、Kryo和Protostuff等。

以下提到的都是基于二进制的序列化协议,而像JSON和XML则属于文本类型的序列化方式。尽管它们具备良好的可读性,但性能相对较差,通常不被选择。

JDK自带的序列化方式

使用JDK的序列化功能,只需实现java.io.Serializable接口即可。

@AllArgsConstructor  
@NoArgsConstructor  
@Getter  
@Builder  
@ToString  
public class RpcRequest implements Serializable {  
    private static final long serialVersionUID = 1905122041950251207L;  
    private String requestId;  
    private String interfaceName;  
    private String methodName;  
    private Object[] parameters;  
    private Class<?>[] paramTypes;  
    private RpcMessageTypeEnum rpcMessageTypeEnum;  
}  

serialVersionUID用于版本控制,序列化时会写入二进制序列,反序列化时会检查serialVersionUID是否一致。如果不一致,则会抛出InvalidClassException异常。强烈建议每个序列化类手动指定其serialVersionUID,以避免使用默认值导致的问题。

我们几乎不直接使用这种序列化方式,主要有两个原因:

  1. 不支持跨语言调用:如果服务是用其他语言开发的,则无法使用。
  2. 性能差:相较于其他序列化框架,JDK的序列化性能较低,序列化后的字节数组体积较大,增加了传输成本。

Kryo

Kryo是一个高性能的序列化/反序列化工具,由于其变长存储特性和字节码生成机制,运行速度快且字节码体积小。

Kryo已经在Twitter、Groupon、Yahoo以及多个知名开源项目(如Hive和Storm)中得到了广泛应用。

guide-rpc-framework就是使用Kryo进行序列化,相关代码如下:

@Slf4j  
public class KryoSerializer implements Serializer {  

    private final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {  
        Kryo kryo = new Kryo();  
        kryo.register(RpcResponse.class);  
        kryo.register(RpcRequest.class);  
        return kryo;  
    });  

    @Override  
    public byte[] serialize(Object obj) {  
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();  
             Output output = new Output(byteArrayOutputStream)) {  
            Kryo kryo = kryoThreadLocal.get();  
            kryo.writeObject(output, obj);  
            kryoThreadLocal.remove();  
            return output.toBytes();  
        } catch (Exception e) {  
            throw new SerializeException("Serialization failed");  
        }  
    }  

    @Override  
    public <T> T deserialize(byte[] bytes, Class<T> clazz) {  
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);  
             Input input = new Input(byteArrayInputStream)) {  
            Kryo kryo = kryoThreadLocal.get();  
            Object o = kryo.readObject(input, clazz);  
            kryoThreadLocal.remove();  
            return clazz.cast(o);  
        } catch (Exception e) {  
            throw new SerializeException("Deserialization failed");  
        }  
    }  
}  

GitHub 地址:https://github.com/EsotericSoftware/kryo

Protobuf

Protobuf是Google推出的高性能序列化工具,支持多种语言且跨平台。然而,使用时需要定义IDL文件并生成相应的序列化代码,虽然不够灵活,却避免了序列化漏洞的风险。

Protobuf包括序列化格式定义,各种语言的库及IDL编译器。通常需定义proto文件,并使用编译器生成目标语言的代码。

以下是一个简单的proto文件示例:

// protobuf的版本  
syntax = "proto3";  
message Person {  
  string name = 1;  
  int32 age = 2;  
}  

GitHub 地址:https://github.com/protocolbuffers/protobuf

ProtoStuff

Protostuff是基于Google Protobuf的增强版,提供了更多功能和更易用性。尽管使用简单,但性能依旧不逊色。

GitHub 地址:https://github.com/protostuff/protostuff

Hessian

Hessian是一种轻量级、可自定义的二进制RPC协议,已经有较长的历史,并且同样支持跨语言的序列化。

图片dubbo RPC默认使用的序列化方式是Hessian2,不过Dubbo对Hessian2进行了修改,结构上仍然相似。

总结

Kryo针对Java语言的序列化方式表现优异,若应用专为Java开发,可以考虑使用。而根据Dubbo官网推荐,Kryo也适合生产环境使用(参考链接:https://dubbo.apache.org/zh/docs/v2.7/user/references/protocol/rest/)。

图片Protobuf、ProtoStuff和Hessian等都是跨语言的序列化方式,如果有跨语言需求也应考虑这些选项。

除了上述协议,Thrift和Avro等也是值得关注的序列化方式。

进一步阅读资源

参考资料

[1] guide-rpc-framework: https://github.com/Snailclimb/guide-rpc-framework

[2] https://github.com/EsotericSoftware/kryo: https://github.com/EsotericSoftware/kryo

[3] https://github.com/protocolbuffers/protobuf: https://github.com/protocolbuffers/protobuf

[4] https://github.com/protostuff/protostuff: https://github.com/protostuff/protostuff

[5] https://dubbo.apache.org/zh/docs/v2.7/user/references/protocol/rest/: https://dubbo.apache.org/zh/docs/v2.7/user/references/protocol/rest/

[6] https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html: https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html

[7] https://dubbo.apache.org/zh-cn/docs/user/serialization.html: https://dubbo.apache.org/zh-cn/docs/user/serialization.html