目录

序列化的意义

序列化面临的挑战

基于JDK 序列化方式实现

序列化的高阶认识

serialVersionUID 的作用

静态变量序列化

父类的序列化

Transient 关键字

常见的序列化技术

JAVA序列化框架

XML 序列化框架

JSON 序列化框架

Hessian 序列化框架

Protobuf 序列化框架

序列化技术的选型


  • 序列化的意义

Java 平台允许我们在内存中创建可复用的Java 对象,但一般情况下,只有当JVM 处于运行时,这些对象才可能存在,即这些对象的生命周期不会比JVM 的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java 对象序列化就能够帮助我们实现该功能。

简单来说,序列化是把对象的状态信息转化为可存储或传输的形式过程,也就是把对象转化为字节序列的过程称为对象的序列化。反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程成为对象的反序列化。

  • 序列化面临的挑战

评价一个序列化算法优劣的两个重要指标是:

一. 序列化以后的数据大小;

二. 序列化操作本身的速度及系统资源开销(CPU、内存)。

Java 语言本身提供了对象序列化机制,也是Java 语言本身最重要的底层机制之一。在Java 中,只要一个类实现了java.io.Serializable 接口,那么它就可以被序列化。

  • 基于JDK 序列化方式实现

JDK 提供了Java 对象的序列化方式,主要通过输出流java.io.ObjectOutputStream 和输入流java.io.ObjectInputStream来实现。其中,被序列化的对象需要实现java.io.Serializable 接口。

import java.io.*;
import java.util.Date;

public class ObjectSaver {
    public static void main(String[] args) throws Exception {
        //序列化对象
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\temp.txt"));
        Customer customer = new Customer("张三", 24);
        out.writeObject("你好!");    //写入字面值常量
        out.writeObject(new Date());    //写入匿名Date对象
        out.writeObject(customer);    //写入customer对象
        out.close();


        //反序列化对象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\temp.txt"));
        System.out.println("obj1 " + (String) in.readObject());    //读取字面值常量
        System.out.println("obj2 " + (Date) in.readObject());    //读取匿名Date对象
        Customer obj3 = (Customer) in.readObject();    //读取customer对象
        System.out.println("obj3 " + obj3);
        in.close();
    }
}

class Customer implements Serializable {
    private String name;
    private int age;
    public Customer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return "name=" + name + ", age=" + age;
    }
}
  • 序列化的高阶认识

  • serialVersionUID 的作用

Java 的序列化机制是通过判断类的serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。

当实现java.io.Serializable 接口的类没有显式地定义一个serialVersionUID 变量时候,Java 序列化机制会根据编译的Class 自动生成一个唯一的serialVersionUID,这种情况下,如果Class 文件的类名、方法名、属性名等没有发生变化,只是增加空格、换行、注释等,就算再编译多次,serialVersionUID 也不会变化。

  • 静态变量序列化

序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。

  • 父类的序列化

一. 当父类没有实现Serializable 接口,子类继承该父类并且实现了Serializable 接口,在反序列化该子类后,将无法获取到父类的属性值的;

二. 当父类实现了Serializable 接口,子类将自动支持序列化功能,不需要再显式实现Serializable 接口;

三. 当一个对象的实例变量引用了其他对象,序列化该对象时也会把引用对象进行序列化,但是前提是该引用对象必须实现了Serializable 接口。

  • Transient 关键字

Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,Transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

绕开Transient 机制实现自定义序列化

/**
  * ObjectOutputStream使用了反射来寻找是否声明了该方法
  * 实现自定义的序列化操作
  */
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException

/**
  * ObjectInputStream使用了反射来寻找是否声明了该方法
  * 实现自定义的反序列化操作
  */
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException

/**
  * ObjectInputStream使用了反射来寻找是否声明了该方法
  * 在使用单例时,如果使用的不是枚举的实现形式,为了保证反序列化出来后的对象,不会破坏单例的情况
  * 使用该方法返回原有实例
  */
private Object readResolve()

以HashMap为例,为何HashMap要自己实现writeObject和readObject方法?

由于大部分情况序列化和反序列化所在的机器是不同的,而序列化和反序列化的一个最基本的要求就是,反序列化之后的对象与序列化之前的对象是一致的。HashMap中,由于Node 的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key 在不同的JVM实现中计算得出的Hash值可能是不同的。 Hash值不同就有可能导致一个HashMap对象的反序列化结果与序列化之前的结果不一致。

为了避免这个问题,HashMap采用了下面的方式来解决:

一. 将可能会造成数据不一致的元素使用transient关键字修饰,从而避免JDK中默认序列化方法对该对象的序列化操作。不序列化的包括:Node<K,V>[] table,Set<Map.Entry<K,V>> entrySet,int size,int modCount。

二. 自己实现writeObject和readObject方法。在序列化的时候不会将保存数据的数组序列化,而是将元素个数以及每个元素的Key和Value都进行序列化。在反序列化的时候,重新计算Key和Value的位置,重新填充一个数组,从而保证序列化和反序列化结果的一致性。 

  • 常见的序列化技术

  • JAVA序列化框架

使用JAVA 进行序列化有他的优点,也有他的缺点。

优点:JAVA 语言本身提供,使用比较方便和简单。

缺点:不支持跨语言处理、 性能相对不是很好,序列化以后产生的数据相对较大。

  • XML 序列化框架

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

  • JSON 序列化框架

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

JSON 序列化常用的开源工具有很多:

  1. Jackson (https://github.com/FasterXML/jackson)
  2. 阿里开源的FastJson (https://github.com/alibaba/fastjon)
  3. Google 的GSON (https://github.com/google/gson)

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

  • Hessian 序列化框架

Hessian 是一个支持跨语言传输的二进制序列化协议,相对于Java 默认的序列化机制来说,Hessian 具有更好的性能和易用性,而且支持多种不同的语言。实际上Dubbo 采用的就是Hessian 序列化来实现,只不过Dubbo 对Hessian 进行了重构,性能更高。

  • Protobuf 序列化框架

Protobuf 是Google 的一种数据交换格式,它独立于语言、独立于平台。Google 提供了多种语言来实现,比如Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件。Protobuf 使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中。但是但是要使用Protobuf 会相对来说麻烦些,因为他有自己的语法,有自己的编译器。

  • 序列化技术的选型

考虑因素:

1. 序列化空间开销,也就是序列化产生的结果大小,这个影响到传输的性能;

2. 序列化过程中消耗的时长,序列化消耗时间过长影响到业务的响应时间;

3. 序列化协议是否支持跨平台,跨语言。因为现在的架构更加灵活,如果存在异构系统通信需求,那么这个是必须要考虑的;

4. 可扩展性/兼容性,在实际业务开发中,系统往往需要随着需求的快速迭代来实现快速更新,这就要求我们采用的序列化协议基于良好的可扩展性/兼容性,比如在现有的序列化数据结构中新增一个业务字段,不会影响到现有的服务;

5. 技术的流行程度,越流行的技术意味着使用的公司多,技术解决方案也相对成熟;

6. 学习难度和易用性。

选型建议:

1. 对性能要求不高的场景,可以采用基于XML 的SOAP 协议;

2. 对性能和间接性有比较高要求的场景,那么Hessian、Protobuf、Thrift、Avro 都可以。

3. 基于前后端分离,或者独立的对外的api 服务,选用JSON 是比较好的,对于调试、可读性都很不错;

4. Avro 设计理念偏于动态类型语言,那么这类的场景使用Avro 是可以的。

 

10-06 21:49