【Java杂记】序列化:从Java序列化到多种序列化方式

1,435 阅读12分钟

1.序列化是什么

  • Java平台允许我们创建对象
  • 但是只有当JVM运行时,这些对象才存在,也就是说对象的声明周期不回避JVM的生命周期长
  • 但我们在硬盘上存储对象,或者通过网络传输对象的时候已经脱离了JVM,已经不是java对象了

==> 需要一种方式:Java对象 ---序列化--> 保存/传输 --反序列化--> Java对象

  • 序列化(编码):
    • 把对象的状态信息转化为可存储或传输的形式过程,也就是把对象转化为字节序列的过程
    • Java对象 --编码协议--> 字节/符序列
  • 反序列化(解码)
    • 把字节数组反序列化为对象,把字节序列恢复为对象的过程
    • 字节/符序列 --解码协议 --> Java对象

序列化与反序列化,也可以理解为:对于Java对象的编码和解码 在这里插入图片描述 注:其中通信协议的作用在于:规定了字节/符序列的格式,从而规定了Java对象如何设计。这里再放一个参考链接...

2.Java原生序列化

原生序列化就是实现Serializable

2.1 使用示例

// JAVA对象,必须实现Serializable才能被序列化和反序列化
public class User implements Serializable{
    private String name;
    
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
}
// Server...读
public static void main(String[] args) throws Exception {
    ServerSocket server = new ServerSocket(8080);
    Socket socket = server.accept();
    ObjectInputStream ois = new ObjectInputStream(socket.getInputStrem());
    // 读,反序列化
    User user = ois.readObject();
   	System.out.Println(user);
}
// Client...写
public static void main(String[] args) throws Exception {
    Socket client = new Socket(8080);
    User = new User();
   	ObjectOutputStrem oos = new ObjectOutputStream(client.getOutputStream());
    // 写,序列化
    out.writeObject(user);
}

2.2 serializable原理

// Serializable是一个空接口
public interface Serializable {
}
  • serialVersionUID(序列化版本号)

    • 标识序列化后的版本号,若在反序列回Java对象时,其相应Java类的SerialVersionUID不同,则无法反序列化,报InvalidCastException(序列化版本不一致)

    • serialVersionUID生成,可以是显式指定,也可以是隐式自动生成

      • 1L:private static final long SerialVersionUID = 1L

      • 根据类名,接口名,成员方法生成一个64位哈希字段(隐式自动生成)

        注:生成后只要属性不变,无论编译多少次SerialVersionUID都不会改变

  • Transient关键字:控制成员变量的序列化

    • 序列化:在成员变量前加上Transient后,该变量不会被序列化到文件中
    • 反序列化:在被反序列化后,该变量会被设为初始值(int=0,obj=null)
    • 作用:常用于容器类的序列化,如ArrayList中的transient Object[] elementData,那么我们可以自定义序列化方式,只存储核心变量,节省了资源。那么自定义呢?向下看...
  • writeObject 和 readObject

    • 对于transient字段,添加 writeObject 和 readObject,告诉IO流如何read和write,就可以实现自定义序列化与反序列化
    • 这两个方法是私有方法,分别位于 ObjectInputStream 和 ObjectOutputStream
    // ArrayList的readObject()
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;
    
        // Read in size, and any hidden stuff
        s.defaultReadObject();
    
        // Read in capacity
        s.readInt(); // ignored
    
        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);
    
            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                // 调用ObjectInputStream的readObject方法
                a[i] = s.readObject();
            }
        }
    }
    

    具体的调用处的源码: 在这里插入图片描述

    void invokeReadObject(Object obj, ObjectInputStream in)
        throws ClassNotFoundException, IOException,
               UnsupportedOperationException
    {
        requireInitialized();
        // 当readObject存在时
        if (readObjectMethod != null) {
            try {
            	// 通过反射调用原类中的readObject方法
            	// 因此,当要为transient字段序列化和反序列化,需要添加readObject与writeObject
                readObjectMethod.invoke(obj, new Object[]{ in });
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof ClassNotFoundException) {
                    throw (ClassNotFoundException) th;
                } else if (th instanceof IOException) {
                    throw (IOException) th;
                } else {
                    throwMiscException(th);
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }
    

2.3 Java序列化总结

  1. Java序列化知识针对对象的状态进行保存,至于类中的方法,序列化并不关心
  2. 当一个父类实现了序列化接口(Serializable),其子类会自动实现序列化,不用再显示实现
  3. 当一个对象的成员变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进行序列化(深度克隆)
  4. 当某个字段被声明为transient后,,默认的序列化机制会忽略这个字段,但可添加writeObject 和 readObject实现自定义序列化

3.分布式下常见序列化技术

随着分布式架构、微服务架构的普及。服务与服务之间的通信成了最基本的需求。这个时候, 我们不仅需要考虑通信的性能,也需要考虑到语言多元化问题。所以,对于序列化来说,如何去提升序列化性能以及解决跨语言问题,就成了一个重点考虑的问题。

由于Java本身提供的序列化机制存在两个问题

  1. 序列化的数据比较大,传输效率低
  2. 其他语言无法识别和对接

以至于在后来的很长一段时间,基于XML格式编码的对象序列化机制成为了主流,一方面解决了多语言兼容问题,另一方面比二进制的序列化方式更容易理解。以至于基于XML的SOAP 协议及对应的WebService框架在很长一段时间内成为各个主流开发语言的必备的技术。 再到后来,基于 JSON 的简单文本格式编码的 HTTP REST 接口又基本上取代了复杂的 Web Service 接口,成为分布式架构中远程通信的首要选择。但是 JSON 序列化存储占用的空间大、性能低等问题,同时移动客户端应用需要更高效的传输数据来提升用户体验。在这种情况下与语言无关并且高效的二进制编码协议就成为了大家追求的热点技术之一。首先诞生的 一个开源的二进制序列化框架-MessagePack。它比 google 的 Protocol Buffers 出现得还要早。

3.1 XML

XML 序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的字节码文件比较大, 而且效率不高,适用于对性能不高,而且QPS较低的企业级内部系统之间的数据交换的场景, 同时XML又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟知的 Webservice,就是采用XML格式对数据进行序列化的。XML序列化/反序列化的实现方式有很多,熟知的方式有XStream和Java自带的XML序列化和反序列化两种

3.2 JSON

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,相对于XML来说,JSON 的字节流更小,而且可读性也非常好。现在JSON数据格式在企业运用是最普遍的 JSON序列化常用的开源工具有很多

  1. Jackson (github.com/FasterXML/j…
  2. 阿里开源的FastJson (github.com/alibaba/fas…
  3. Google的GSON (github.com/google/gson)

这几种 json 序列化工具中,Jackson 与 fastjson 要比 GSON 的性能要好,但是 Jackson、 GSON的稳定性要比Fastjson好。而fastjson的优势在于提供的api非常容易使用

3.3 Protobuf

Protobuf是Google的一种数据交换格式,它独立于语言、独立于平台。Google提供了多种语言来实现,比如Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件, Protobuf是一个纯粹的表示层协议,可以和各种传输层协议一起使用。

Protobuf使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要 求高的 RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中 但是要使用Protobuf会相对来说麻烦些,因为他有自己的语法,有自己的编译器,如果需要 用到的话必须要去投入成本在这个技术的学习中

protobuf有个缺点就是要传输的每一个类的结构都要生成对应的proto文件,如果某个类发生修改,还得重新生成该类对应的proto文件

3.4Hessian

Hessian是一个支持跨语言传输的二进制序列化协议,相对于Java默认的序列化机制来说, Hessian具有更好的性能和易用性,而且支持多种不同的语言

实际上 Dubbo 采用的就是 Hessian 序列化来实现,只不过 Dubbo 对 Hessian 进行了重构, 性能更高

3.5 Avro

Avro是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷,快速地处理大量数据;动态语言友好,Avro提供的机制使动 态语言可以方便地处理Avro数据。

3.6 Kyro

Kryo是一种非常成熟的序列化实现,已经在Hive、Storm)中使用得比较广泛,不过它不能跨语言. 目前 dubbo 已经在 2.6 版本支持 kyro 的序列化机制。它的性能要优于之前的 hessian2

4.Protobuf序列化原理*

那么接下来着重分析一下protobuf的序列化原理,前面说过它的优势是空间开销小,性能也相对较好。

4.1 基本使用

使用protobuf开发的一般步骤是

  1. 配置开发环境,安装protocol compiler代码编译器

    https://github.com/protocolbuffers/protobuf/releases,    找到protoc-3.5.1-win32.zip 
    
  2. 编写.proto文件,定义序列化对象的数据结构。语法大概就三种:数据类型,修饰符和表示顺序(左边注释) 在这里插入图片描述

  3. 基于编写的.proto文件,使用protocol compiler编译器生成对应的序列化/反序列化工具类

    .\protoc.exe --java_out=./    ./user.proto
    
  4. 基于自动生成的代码,编写自己的序列化应用

在这里插入图片描述

我们可以看到,序列化出来的数字基本看不懂,但是序列化以后的数据确实很小,那么要达到最小的序列化结果,一定会用到压缩的技术,而protobuf用到了两种压缩算法,varint 和 zigzag。下面我们就看看 varint 的压缩原理是什么。

4.2 压缩原理

  1. Mic这个字符如何被压缩的?

    “Mic”这个字符,需要根据ASCII对照表转化为数字。 M =77、i=105、c=99 所以结果为 77 105 99

    这里的结果为什么直接就是 ASCII 编码的值呢?怎么没有做压缩呢?原因是,varint是对字节码做压缩,但是如果这个数字的二进制只需要一个字节表示的时候, 其实最终编码出来的结果是不会变化的

  2. age=300这个数字是如何被压缩的?过程如下图 在这里插入图片描述 这两个字节字节分别的结果是:-84 和 2。 再说一下 -84 怎么计算来的把,我们知道在二进制中表示负数的方法,高位设置为1, 并且是对应数 字的二进制取反以后再计算补码表示(补码是反码+1) 所以如果要反过来计算

    1. 【补码】10101100 -1 得到 10101011
    2. 【反码】01010100 得到的结果为84. 由于高位是1,表示负数所以结果为-84

4.3 存储格式

还有两个数字,3 和16代表什么呢?那就要了解protobuf的存储格式了。protobuf采用T-L-V作为存储方式

在这里插入图片描述 tag的计算方式是 field_number(当前字段的编号) << 3 | wire_type

  • 比如Mic的字段编号是1 ,类型wire_type的值为 2 所以 : 1 <<3 | 2 =10
  • age=300的字段编号是2,类型wire_type的值是0, 所以 : 2<<3|0 =16

第一个数字10,代表的是key,剩下的都是value。

4.4 总结

Protocol Buffer的性能好,主要体现在 序列化后的数据体积小 & 序列化速度快,最终使得传输效率高,其原因如下:

  • 序列化速度快的原因:
    1. 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等)
    2. 采用 Protocol Buffer 自身的框架代码 和 编译器共同完成
  • 序列化后的数据量体积小(即数据压缩效果好)的原因:
    1. 采用了独特的编码方式,如Varint、Zigzag编码方式等等
    2. 采用T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑

5.序列化技术的选型

5.1 技术层面

  1. 序列化空间开销:即序列化产生结果的大小,这个影响传输性能
  2. 序列化过程中消耗的时长:序列化消耗的时间过长,势必影响业务的相应时间
  3. 序列化协议是否支持跨平台,跨语言:现在架构更加灵活,若存在异构通信,这点必须考虑
  4. 可扩展性、兼容性:系统的更新迭代快, 若要在现有序列化数据结构中增加一个字段,不会影响原有业务
  5. 技术的流行程度:越流行的技术意味着使用公司多,升级改进和解决bug更快捷

5.2 具体建议

  1. 对性能要求不高的场景:可以采用XML的SOAP协议
  2. 对性能和间接性有较高要求的场景:Hessian、Protobuf、Thrift、Avro均可
  3. 前后端分离,独立的api服务:选用json较好,对于调试、可读性都不错
  4. 动态类型语言:Avro,它的设计更偏向于动态类型语言