分布式四、序列化与反序列化

Valentine 转载请标明出处。

序列化的意义

实体在网络中通信

Java 平台允许我们在内存中创建可复用的Java 对象,但一般情况下,只有当JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM 的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后还能够保存(持久化)指定的对象,并在将来重新读取被保存的对象,Java 对象的序列化就能够实现该功能。
简单来说
序列化是把对象的状态信息转化为可存储或传输的形式过程,就是把对象转化为字节序列的称为对象的序列化,反序列化是序列化的逆向过程,把字节序列恢复为对象的过程称为对象的反序列化。

序列化面临的挑战

评价一个序列化算法优劣的两个重要指标是:
1、序列化以后的数据大小;
2、序列化操作本身的速度及系统资源开销(CPU、内存);
Java 语言本身提供了对象序列化机制,也是Java 语言本身最重要的底层机制之一,但是Java 本身提供的序列化机制存在两个问题:

  1. 序列化的数据比较大,传输效率低
  2. 其他语言无法识别和对接
    如何实现一个序列化操作
    在Java 中,只要一个类实现了java.io.Serializable 接口,那么它就可以被序列化
    定义接口
public interface ISerializer {
    // 序列化
    <T> byte[] serializer(T obj);
    // 反序列化
    <T> T deSerializer(byte[] data,Class<T> clazz);
}

public class JavaSerializer implements ISerializer {

    @Override
    public <T> byte[] serializer(T obj) {
        ObjectOutputStream objectOutputStream=null;
        try {
            objectOutputStream=new ObjectOutputStream(new FileOutputStream(new File("user")));
            objectOutputStream.writeObject(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(objectOutputStream!=null){
                try {
                    objectOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    @Override
    public <T> T deSerializer(byte[] data, Class<T> clazz) {
        ObjectInputStream objectInputStream=null;
        try {
            objectInputStream=new ObjectInputStream(new FileInputStream(new File("user")));
            return (T)objectInputStream.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if(objectInputStream!=null){
                try {
                    objectInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

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

public class SuperClass implements Serializable {

    String sex;
    ...
}

public class User extends SuperClass {

    public static int num=5;

    private String name;

    private int age;

    private transient String hobby;

    //序列化对象
    private void writeObject(ObjectOutputStream objectOutputStream) throws IOException {
        objectOutputStream.defaultWriteObject();
        objectOutputStream.writeObject(hobby);
    }

    //反序列化
    private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
        objectInputStream.defaultReadObject();
        hobby=(String)objectInputStream.readObject();
    }
     ...
}

public static void main(String[] args) {
        JavaSerializer javaSerializer = new JavaSerializer();
        User user = new User();
        user.setAge(18);
        user.setName("Valentine");
        user.setHobby("健身");
        User user1 = iSerializer.deSerializer(null, User.class);
    }

序列化的高阶认识

serialVersionUID 的作用,Java 的序列化机制是通过判断类的serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException如果没有为指定的class 配置serialVersionUID,那么java 编译器会自
动给这个class 进行一个摘要算法,类似于指纹算法,只要这个文件有任何改动,得到的UID 就会截然不同的,可以保证在这么多类中,这个编号是唯一的。
serialVersionUID 有两种显示的生成方式:
一是默认的1L,比如:private static final long serialVersionUID = 1L;
二是根据类名、接口名、成员方法及属性等来生成一个64 位的哈希字段;
当实现java.io.Serializable 接口的类没有显式地定义一个serialVersionUID 变量时候,Java 序列化机制会根据编译的Class 自动生成一个serialVersionUID 作序列化版本比较用,这种情况下,如果Class 文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等
等),就算再编译多次,serialVersionUID 也不会变化的。

静态变量序列化

public class App {
    public static void main(String[] args) {
        JavaSerializer javaSerializer = new JavaSerializer();
        User user = new User();
        user.setAge(18);
        user.setName("Valentine");
        user.setHobby("健身");
        user.num = 10;
        User user1 = iSerializer.deSerializer(null, User.class);
        System.out.println(user1.num);
    }
}

在User 中添加一个全局的静态变量num , 在执行序列化以后修改num 的值为10, 然后通过反序列化以后得到的对象去输出num 的值,然后通过反序列化以后得到的对象去输出num 的值最后的输出是 10,理论上打印的 num 是从读取的对象里获得的,应该是保存时的状态才对。之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。

父类的序列化

一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable接口,在子类中设置父类的成员变量的值,接着序列化该子类对象。再反序列化出来以后输出父类属性的值。结果应该是什么?
发现父类的字段的值为null。也就是父类没有实现序列化。
结论:

  1. 当一个父类没有实现序列化时,子类继承该父类并且实现了序列化。在反序列化该子类后,是没办法获取到父类的属性值的;
  2. 当一个父类实现序列化,子类自动实现序列化,不需要再显示实现Serializable 接口;
  3. 当一个对象的实例变量引用了其他对象,序列化该对象时也会把引用对象进行序列化,但是前提是该引用对象必须实现序列化接口;

Transient 关键字

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

绕开transient 机制的办法

writeObject和readObject 这两个私有的方法,既不属于Object、也不是Serializable,为什么能够在序列化的时候被调用呢?
原因是,ObjectOutputStream使用了反射来寻找是否声明了这两个方法。因为ObjectOutputStream使用getPrivateMethod,所以这些方法必须声明为private 以至于供ObjectOutputStream 来使用。

序列化的存储规则

public class StoreRuleDemo {

    public static void main(String[] args) throws IOException {
        ObjectOutputStream outputStream =
                new ObjectOutputStream(new FileOutputStream(new File("user")));
        User user = new User();
        user.setAge(18);
        user.setName("Valentine");
        user.setHobby("健身");
        user.setSex("男");
        outputStream.flush();
        outputStream.writeObject(user);
        System.out.println(new File("user").length());
        outputStream.writeObject(user);
        outputStream.flush();
        outputStream.close();
        System.out.println(new File("user").length());
    }
}

同一对象两次(开始写入文件到最终关闭流这个过程算一次,上面的演示效果是不关闭流的情况才能演示出效果)写入文件,打印出写入一次对象后的存储大小和写入两次后的存储大小,第二次写入对象时文件只增加了 5 字节。
Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,该存储规则极大的节省了存储空间。

序列化实现深克隆

在Java 中存在一个Cloneable 接口,通过实现这个接口的类都会具备clone 的能力,同时clone 是在内存中进行,在性能方面会比直接通过new生成对象要高一些,特别是一些大的对象的生成,性能提升相对比较明显。那么在Java 领域中,克隆分为深度克隆和浅克隆。
浅克隆
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。

public class Email implements Serializable {

    private String content;
    ...
}

public class Person implements Cloneable, Serializable {

    private String name;

    private Email email;
    ...

    @Override
    protected Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }

    public Person deepClone() throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream =
                new ObjectOutputStream(bos);
        objectOutputStream.writeObject(this);

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(bis);
        return (Person) objectInputStream.readObject();

    }
}

public class CloneDemo {

    public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
        Email email=new Email();
        email.setContent("八点回家");
        Person p1=new Person("Valentine");
        p1.setEmail(email);

//        Person p2=p1.clone();
        Person p2=p1.deepClone();
        p2.setName("Sam");
        p2.getEmail().setContent("9点回家");

        System.out.println(p1.getName()+"->"+p1.getEmail().getContent());
        System.out.println(p2.getName()+"->"+p2.getEmail().getContent());

    }
}

深克隆
被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
实现深克隆效果的原理是把对象序列化输出到一个流中,然后在把对象从序列化流中读取出来,这个对象就不是原来的对象了。

常见的序列化技术

使用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 会相对来说麻烦些,因为他有自己的语法,有自己的编译器。
总结
Protocol Buffer 的性能好,主要体现在 序列化后的数据体积小 & 序列化速度快,最终使得传输效率高,其原因如下:
序列化速度快的原因:
1、 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等);
2、采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成序列化后的数据量体积小(即数据压缩效果好)的原因:
a. 采用了独特的编码方式,如Varint、Zigzag 编码方式等等;
b. 采用T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑;
各个序列化技术的性能比较,这个地址有针对不同序列化技术进行性能比较:
https://github.com/eishay/jvm-serializers/wiki

序列化技术的选型

技术层面

1、序列化空间开销,也就是序列化产生的结果大小,这个影响到传输的性能。
2、序列化过程中消耗的时长,序列化消耗时间过长影响到业务的响应时间。
3、序列化协议是否支持跨平台,跨语言。因为现在的架构更加灵活,如果存在异构系统通信需求,那么这个是必须要考虑的。
4、可扩展性/兼容性,在实际业务开发中,系统往往需要随着需求的快速迭代来实现快速更新,这就要求我们采用的序列化协议基于良好的可扩展性/兼容性,比如在现有的序列化数据结构中新增一个业务字段,不会影响到现有的服务。
5、技术的流行程度,越流行的技术意味着使用的公司多,那么很多坑都已经淌过并且得到了解决,技术解决方案也相对成熟。
6、学习难度和易用性。

选型建议

1、对性能要求不高的场景,可以采用基于XML 的SOAP 协议。
2、对性能和间接性有比较高要求的场景,那么Hessian、Protobuf、Thrift、Avro 都可以。
3、基于前后端分离,或者独立的对外的api 服务,选用JSON 是比较好的,对于调试、可读性都很不错。
4、Avro 设计理念偏于动态类型语言,那么这类的场景使用Avro 是可以的。

学习来源https://www.gupaoedu.com/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342