• java序列化和反序列化


    java领域的对象如何传输

    基于socket进行对象传输

    如果传输的对象没有序列化会报错:

    那么如何解决这个问题呢?

    实现一个Serializable接口,再次运行就可以看到对象可以正常传输了

    public class User implements Serializeable{
      
    }
    

    序列化的意义

    当我们对User这个类增加一个Serializable,就可以解决java对象的网络传输问题。这就是序列化的意义。

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

    简单来说

    序列化是把对象的状态信息转化为可存储或传输的形式过程,也就是把对象转化为字节序列的过程称为对象的序列化。

    反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程成为对象的反序列化。

    序列化的进阶

    java原生序列化

    主要通过输出流java.io.ObjectOutputStream和对象输入流java.io.ObjectInputStream来实现。

    java.io.ObjectOutputStream:表示对象输出流,它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

    java.io.ObjectInputStream:表示对象输入流,它的readObject()方法源输入流中读取字节序列,在把他们反序列化成为一个对象,并将其返回。

    需要注意的是,被序列化对象需要实现java.io.Serializeable接口

    serialVersionUID的作用

    设置serialVersionUID,字面的意思是序列化的版本号,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量。

    证明操作:

    1.先将user对象序列化到文件中

    2.然后修改user对象,增加serialVersionUID字段

    3.然后通过反序列化来把对象提取出来

    4.结果:无法反序列化

    结论

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

    从结果可以看出,文件流中的class和classpath中的class,也就是修改过后的class,不兼容了,处于安全机制考虑,程序抛出了错误,并且拒绝载入。从错误结果来看,如果没有为指定的class配置serialVersionUID,那么java编译器会自动给这个class进行一个摘要算法,类似于指纹算法,只要这个文件有任何改动,得到的UID就会截然不同的,可以保证在这么类中,这个编号是唯一的。所以,由于没有现实指定serialVersionUID,编译器有为我们生成了一个UID,当然和前面保存在文件中的那个不会一样,于是就出现了2个序列号不一致的错位,因此,只要我们自己指定可serialVersionUID,就可以在序列化后,去添加一个字段,或者方法,而不会影响到后期的还原,还原后的对象照样可以使用,而且还多个方法或者属性可以用。

    ** tips:serialVersionUID有两种显示的生成方式:**

    一是默认的1L,比如:private static final long serialVersionUID = 1L;

    二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段

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

    Transient关键字

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

    绕开transient机制的方法

    private transient String name;
    

    如上虽然name被transient修饰,但是通过我们写的两个方法依然能够使得name字段正确被序列化和反序列化

    writeObject和readObject原理

    writeObject和readObject是两个私有的方法,他们是什么时候被调用呢?从运行结果来看,他确实被调用了。而且他们并不存在于java.lang.Object,也没有在Serializable中去声明。

    从源码层面上来分析可以看到,readObject是通过反射来调用的。

    其实我们可以在很多的地方看到readObject和writeObject的使用,比如HashMap。

    为什么有的代码属性使用了transient修饰符 还要使用writeObject和readObject?

    或者说为什么要自己实现writeObject和readObject方法,而不是使用JDK统一的默认序列化和反序列化操作呢?

    这里以HashMap为例:首先要明确序列化的目的,将java对象序列化,一定是为了在某个时刻能够将该对象反序列化,而且一般来讲序列化和反序列化所在的机器是不同的,因为序列化最常用的场景就是跨机器的调用,而序列化和反序列化的一个最基本的要求就是,反序列化之后的对象与序列化之前的对象是一致的。
    
    HashMap中,由于Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的。
    
    Hash值不同导致的结果就是:有可能一个HashMap对象的反序列化结果与序列化之前的结果不一致。即有可能序列化之前,Key=’AAA’的元素放在数组的第0个位置,而反序列化值后,根据Key获取元素的时候,可能需要从数组为2的位置来获取,而此时获取到的数据与序列化之前肯定是不同的。
    

    解决方式:

    将可能会造成数据不一致的元素使用transient关键字修饰,从而避免JDK中默认序列化方法对该对象的序列化操作。不序列化的包括:Entry[] table,size,modCount。
    
    自己实现writeObject方法,从而保证序列化和反序列化结果的一致性。
    
    那么,HashMap又是通过什么手段来保证序列化和反序列化数据的一致性的呢?
    
    首先,HashMap序列化的时候不会将保存数据的数组序列化,而是将元素个数以及每个元素的Key和Value都进行序列化。
    
    在反序列化的时候,重新计算Key和Value的位置,重新填充一个数组。
    
    想想看,是不是能够解决序列化和反序列化不一致的情况呢?
    
    由于不序列化存放元素的Entry数组,而是反序列化的时候重新生成,这样就避免了反序列化之后根据Key获取到的元素与序列化之前获取到的元素不同。
    

    java序列化的一些简单总结

    1.java序列化只是针对对象的状态进行保存,至于对象中的方法,序列化不关心

    2.当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口

    3.当一个对象的实例变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进行序列化(实现深度克隆)

    4.当某个字段被申明为transient后,默认的序列化机制会忽略这个字段

    5.被申明为transient的字段,如果需要序列化,可以添加两个私有方法:writeObject和readObject

    分布式架构下常见序列化技术

    序列化的发展:

    随着分布式架构、微服务架构的普及。服务与服务之间的通信成了最基本的需求。这个时候,我们不仅需要考虑通信的性能,也需要考虑到语言多元化问题。

    所以,对于序列化来说,如何去提升序列化性能以及解决跨语言问题,就成了重点解决的问题。

    java本身提供的序列化存在两个问题:

    1.序列化的数据比较大,传输效率低

    2.其他语言无法识别和对接

    以至于在后来的很长一段时间,基于XML格式编码的对象序列化机制成为主流,一方面解决了多语言兼容问题,另一方面比二进制更容易理解。以至于基于XML的SOAP协议及对应的WebService框架在很长一段时间内成为各个主流开发语言的必备的技术。

    再后来,基于Json的简单文本格式编码的HTTP REST接口又基本上取代了复杂的WebService接口,成为分布式架构中远程通信的首要选择。但是Json序列化存储占用的空间大、性能低等问题,同时移动客户端应用需要更高效的传输数据来提升用户体验。在这种情况下与语言无关并且高效的二进制编码协议就成为追求的热点技术之一。最先诞生的是开源二进制序列化框架--MessagePack。它比Protocol Buffers出现的还要早。

    各种序列化技术

    XML序列化框架介绍

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

    JSON序列化框架

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

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

    1.Jackson(https://github.com/FasterXML/jackson)

    2.阿里开源的FastJson(https://github.com/alibaba/fastjson)

    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进行了重构,性能更高。

    Avro序列化

    链接:https://avro.apache.org/

    Avro是一个数据序列系统,设计用于大批量数据交换的应用。由Hadoop的创始人Doug Cutting(也是Lucene,Nutch等项目的创始人)开发。特点:二进制序列化方式,可以便捷,快速的处理大量数据;动态语言友好,Avro提供的机制使动态语言可以方便的处理Avro数据。

    Kryo序列化框架

    链接:https://github.com/EsotericSoftware/kryo/blob/master/README.md

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

    Protobuf序列化框架

    Protobuf是Google的一种数据交换格式,他独立于语言、独立于平台。Google提供了多种语言来实现,比如Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件。

    Protobuf是一个纯粹的表示层协议,可以和各种传输层协议一起使用。

    protobuf使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的RPC调用。

    另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在独享的持久化场景中。

    但是要使用Protobuf会相对来说麻烦一些,因为他有自己的语法,有自己的编译器,如果需要用到的话必须要去投入成本在这个技术的学习中。

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

    Protobuf序列化的原理

    protobuf的基本应用

    使用protobuf开发的一般步骤:

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

    2.编写.proto文件,定义序列化对象的数据结构

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

    4.基于自动生成的代码,编写自己的序列化应用

    Protobuf案例演示

    下载protobuf工具 地址:https://github.com/google/protobuf/releases

    这里提供mac的安装方式:https://www.jianshu.com/p/67f64307d268

    编写proto文件

    syntax="proto2";
    package com.demo.serial;
    option java_package = "com.demo.serial";
    option java_outer_classname = "UserProtos";
    
    message User{
    	required string name = 1;
    	required int32 age = 2;
    }
    
    
    数据类型:
    string/bytes/bool/int32(4个字节)/int64/float/double/enum/message自定义类
    修饰符
    required  表示必填字段
    optional  表示可选字段
    repeated  可重复,表示集合
    1,2,3,4需要在当前范围内是唯一的,表示顺序
    

    生成实体类

    【protoc --java_out=./ user.proto】

    实现序列化

    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java</artifactId>
        <version>3.13.0</version>
    </dependency>
    

    protobuf序列化原理

    我们可以把序列化以后的数据打印出来

    public class TestProto {
        public static void main(String[] args) {
            UserProtos.User user = UserProtos.User.newBuilder().setAge(300).setName("zhangsan").build();
            byte[] bytes = user.toByteArray();
            for (byte bt : bytes) {
                System.out.print(bt + " ");
            }
        }
    }
    //10 8 122 104 97 110 103 115 97 110 16 -84 2
    

    这里可以看到,序列化出来的数字基本不理解,但是序列化以后的数据确实很小。

    底层原理:

    正常来说,要达到最小的序列化结果,一定会用到压缩的技术,而protobug里面用到了两种压缩算法,一种是varint,另一种是zigzag。

    varint

    第一种,我们现来看age=300这个数字是如何被压缩的

    这两个字节分别的结果是:-84 、2

    -84是如何计算出来的呢?我们知道在二进制中表示负数的方法,高位设置为1,并且是对应数字的二进制取反以后在再补码表示(补码是反码+1)

    所以如果要反过来计算:

    1.【补码】10101100 -1 得到10101011

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

    字符转化位编码

    “zhangsan”这个字符,需要根据ASCII对照表转化为数字

    z = 122、 h = 104 、 a=97 、n = 110 、 g = 103 、 s = 115 、 a = 97 、 n= 110

    所以结果为122 104 97 110 103 115 97 110

    为什么这里的记过对应ASCII的值呢?为什么没有压缩呢?

    varint是对字节码做压缩,但是如果这个数字的二进制只需要一个字节表示的时候,其实最终编码出来的结果是不会变化的。
    

    还有两个数字,8和16代表什么呢?这里需要了解protobuf的存储格式了

    存储格式

    tag的计算方式:

    field_number(当前字段的编号) <<3 |write_type

    比如zhangsan的字段编号为1,类型write_type的值为2 所以 1<<3 |2 =10

    zhangsan的长度是8

    age=300的字段编号为2,类型write_type的值为0 所以 2 << 3 |0 = 16

    int类型的长度 这里可以省略不写

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

    负数的存储

    在计算机中,负数会被表示为很大的整数,因为计算机定义负数符号位为数字的最高位,所以如果采用varint编码表示一个负数,那么一定需要5个比特位。所以在protobuf中通过sint32/sint64类型来表示负数,负数的处理形式是采用zigzag编码(吧符号数转化为无符号数),在采用varint编码。

    sint32:(n<<1)^(n>>31)

    sint64:(n<<1)^(n>>63)

    比如存储一个(-300)的值

    -300

    源码:0001 0010 1100

    取反:1110 1101 0011

    加1: 1110 1101 0100

    n<<1:整体左移一位,右边补0 -> 1101 1010 1000

    n>>31:整体右移31位,左边补1 -> 1111 1111 1111

    n<<1 ^ n>>31

    1101 1010 1000 ^ 1111 1111 1111 = 0010 0101 0111

    十进制: 0010 0101 0111 = 599

    varint算法:从右往左,选取7位,高位补1/0(取决于字节数)

    得到两个字节

    1101 01111 0000 0100

    -41 4

    总结

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

    1.编码 / 解码 方式简单 (只需要简单的数学运算 = 位移)

    2.采用proto buffer自身的框架代码和编译器共同完成

    序列化的数据量体积小(即数据压缩效果好)的原因:

    1.采用了独特的编码方式,如varint、zigzag编码方式。

    2.采用T-L-V的数据存储方式:减少了分割符的使用 & 数据存储的紧凑

    序列化的选型

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

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

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

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

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

    6.学习的难度和易用性

    选型建议

    对于性能不高的场景,可以采用基于XML的SOAP协议

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

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

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

    各个序列化技术的性能比较

    针对不同序列化技术进行性能比较 : https://github.com/eishay/jvm-serializers/wiki

  • 相关阅读:
    20145223《Java程序程序设计》课程总结
    20145223《Java程序程序设计》第10周学习总结
    20145223《Java程序程序设计》实验报告5
    20145223《Java程序程序设计》第9周学习总结
    20145223 《Java程序程序设计》实验报告4
    20145223《Java程序程序设计》第8周学习总结
    20145223《Java程序设计》实验报告3
    20145223《Java程序程序设计》第7周学习总结
    20145223《Java程序程序设计》实验报告二
    node_promise
  • 原文地址:https://www.cnblogs.com/snail-gao/p/13891908.html
Copyright © 2020-2023  润新知