序列化和反序列化

序列化和反序列化

    • 什么是序列化
    • 序列化高阶认识
      • jdk序列化
      • serialVersionUID 的作用
      • Transient 关键字
      • 绕开 transient 机制的办法
      • 序列化小结
    • 分布式架构下常见序列化技术
      • 了解序列化的发展
      • 简单了解各种序列化技术
        • json序列化框架
        • Hessian 序列化框架
        • Avro 序列化
        • kyro 序列化框架
        • Protobuf 序列化框架
        • 序列化技术的选型

什么是序列化

序列化:就是把对象从对象形式变成字节序列
反序列化:字节序列形式恢复成对象形式的过程

序列化高阶认识

jdk序列化

对象类 User.java

public class User implements Serializable
{
    private String name;
    
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User [name=" + name + ", age=" + age + "]";
    }
    
    
}

服务端

public class Serversocket 
{
    public static void main(String[] args) throws IOException
    {
        //建立一个server
        ServerSocket serverSocket=null;
        BufferedReader in=null;
        
        try{
            serverSocket=new ServerSocket(8081);
            Socket socket=serverSocket.accept();
            ObjectInputStream objectInputStream=
            new
            ObjectInputStream(socket.getInputStream());
            User user=(User)objectInputStream.readObject();
            System.out.println(user);
            }catch (Exception e){
            e.printStackTrace();
             }finally {
            if(in!=null){
            try {
            in.close();
            } catch (IOException e) {
            e.printStackTrace();
            }
            }
            if(serverSocket!=null){
            serverSocket.close();
            }
            }
                
    }
}

客户端

public class ClientSocket 
{
    public static void main(String[] args) 
    {
        Socket socket=null;
        ObjectOutputStream out=null;
        
        try {
            socket=new Socket("127.0.0.1",8081);
            User user=new User();
            user.setName("小明");
            user.setAge(18);
            out=new
            ObjectOutputStream(socket.getOutputStream());
            out.writeObject(user);
            } catch (IOException e) {
            e.printStackTrace();
            }finally {
            if(out!=null){
            try {
            out.close();
            } catch (IOException e) {
            e.printStackTrace();
            }
             }
            if(socket!=null){
            try {
            socket.close();
            } catch (IOException e) {
            e.printStackTrace();
            }
            }
            }
    }
}

执行结果:

主要通过输出流 ObjectOutputStream和对象输入流ObjectInputStream实现
writeObject 序列化,把obj实例化后输出到流中
readObject 反序列化,输入流中读取字节序列化,反序列化成对象

需要序列化的对象只要实现Seriallizable接口就行

serialVersionUID 的作用

相当于加解密的秘钥,做安全校验,保证序列化和反序列化是同一个对象的操作

Transient 关键字

transient:不允许序列化,序列化的对象中被transient修饰的属性 int值是0,引用型的是null

绕开 transient 机制的办法

writeObject/readObject:
加粗样式被transient修饰的字段属性,通过这两个方法操作,又能序列化和反序列化了,名字必须固定,因为一定会通过反射调用这两个方法

代码如下:

private void writeObject(java.io.ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();

        s.writeObject(name);
    }

    private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        name=(String)s.readObject();
    }

writeObject/readObject:原理
这两个方法不在Object也不在Serializable中,而是跟ObjectInputStream 和 ObjectOutputStream有关
序列化和反序列化_第1张图片
从源码层面来分析可以看到,readObject 是通过反射来调用的。
被transient修饰的字段属性,通过这两个方法操作,又能序列化和反序列化了,名字必须固定,因为一定会通过反射调用这两个方法
其实我们可以在很多地方看到 readObject 和 writeObject 的使用,比如 HashMap。

序列化小结

Java 序列化的一些简 单总结

  1. Java 序列化只是针对对象的状态进行保存,至于对象中的方法,序列化不关心
  2. 当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口
  3. 当一个对象的实例变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进
    行序列化(实现深度克隆)
  4. 当某个字段被申明为 transient 后,默认的序列化机制会忽略这个字段
  5. 被申明为 transient 的字段,如果需要序列化,可以添加两个私有方法:writeObject 和
    readObject

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

了解序列化的发展

随着分布式架构、微服务架构的普及。服务与服务之间的通信成了最基本的需求。
怎么样传输大量数据提高性能呢?

简单了解各种序列化技术

XML序列化框架
优点:阅读方便
缺点:序列化后数据大
熟知的方式有 XStream 和 Java 自带的 XML 序列化和反序列化两种

json序列化框架

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

Avro 序列化

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

kyro 序列化框架

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

Protobuf 序列化框架

Protobuf 是 Google 的一种数据交换格式
优点:主要是空间开销小和性能比较好,
缺点:因为他有自己的语法,有自己的编译器,需要学习下语法;
每一个类的结构都要生成对应的 proto 文件,如果某个类发
生修改,还得重新生成该类对应的 proto 文件
Protobuf 序列化的原理
protobuf 的基本应用
使用 protobuf 开发的一般步骤是

  1. 配置开发环境,安装 protocol compiler 代码编译器
  2. 编写.proto 文件,定义序列化对象的数据结构
  3. 基于编写的.proto 文件,使用 protocol compiler 编译器生成对应的序列化/反序列化工具
  4. 基于自动生成的代码,编写自己的序列化应用
    Pf rotobuf 案例演示
    1、下载 protobuf 工具
    https://github.com/google/protobuf/releases 找到 protoc-3.5.1-win32.zip
    在proto.exe同目录下编写文件User.proto,内容为:
syntax="proto2";
	package com.test.socket;
	option java_package =
	"com.test.socket";
	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.exe --java_out=./ ./user.proto
通过protoc.exe执行,输出路径为当前路径,源文件为user.proto,执行完成后生成UserProtos.java文件

pom.xml引入依赖包


com.google.protobuf

protobuf-
java
3.7.0

java代码

public static void main(String[] args) {
UserProtos.User user=UserProtos.User.newBuilder().
setAge(300).setName("Mic").build();
byte[] bytes=user.toByteArray();
for(byte bt:bytes){
System.out.print(bt+" ");
}
}

执行结果: ➢ 10 3 77 105 99 16 -84 2
看起来优点看不懂,来了解下底层原理

了解底层原理
protobuf用到了两种压缩算法,一种是varint,另一种是 zigzag
varint
看下age=300如何压缩的
序列化和反序列化_第2张图片
这两个字节字节分别的结果是:-84 、2
-84 怎么计算来的呢? 我们知道在二进制中表示负数的方法,高位设置为 1, 并且是对应数
字的二进制取反以后再计算补码表示(补码是反码+1)
所以如果要反过来计算

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

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

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

存储格式
3 和 16 代表什么呢?
protobuf 采用 T-L-V 作为存储方式
序列化和反序列化_第3张图片
序列化和反序列化_第4张图片
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。

负数的存储
在计算机中,负数会被表示为很大的整数,因为计算机定义负数符号位为数字的最高位,所
以如果采用 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 0111 0000 0100
-41 、 4
总结
Protocol Buffer 的性能好,主要体现在 序列化后的数据体积小 & 序列化速度快,最终使得
传输效率高,其原因如下:
序列化速度快的原因:
a. 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等)
b. 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成
序列化后的数据量体积小(即数据压缩效果好)的原因:
a. 采用了独特的编码方式,如 Varint、Zigzag 编码方式等等
b. 采用 T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑

序列化技术的选型

技术层面

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

选型建议

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

各个序列化技术的性能比较
通过序列化同一对象得到的数据大小,比较这几种序列化方式的性能
如user对象序列化
protobuf(7),json(23),hessian(50),java原生(92),xml(198)
可以看出protobuf性能确实很棒,xml,令人发指。

比较集中常用序列化性能:https://github.com/eishay/jvm-serializers/wiki

你可能感兴趣的:(序列化,分布式(基础))