序列化方式
1、Java序列化技术
1.1基础概念
Java 序列化是指把 Java 对象转换为字节序列的过程;(编码)
Java 反序列化是指把字节序列恢复为 Java 对象的过程;(解码)
1.2实现序列化和反序列化
使用到JDK中ObjectOutputStream(对象输出流) 和ObjectInputStream(对象输入流)
ObjectOutputStream 类中:通过使用 writeObject(Object object) 方法,将对象以二进制格式进行写入。
ObjectInputStream 类中:通过使用 readObject()方法,从输入流中读取二进制流,转换成对象。
使用
@Data
public class User implements Serializable {
private static final long serialVersionUID = 5645895533877005194L;
private String name;
private Integer age;
}
//测试
public static void main(String[] args) throws Exception {
//jdk 序列化
FileOutputStream fileOutputStream = new FileOutputStream("user");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(new User("test", 12));
//jdk反序列化
FileInputStream fileInputStream = new FileInputStream("user");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
User user = (User) objectInputStream.readObject();
System.out.println(user);
}
说明:
1、Serializable 接口的作用只是用来标识我们这个类是需要进行序列化,并且 Serializable 接口中并没有提供任何方法。
2、serialVersionUID 序列化版本号的作用是用来区分我们所编写的类的版本,用于判断反序列化时类的版本是否一直,如果不一致会出现版本不一致异常InvalidCastException。
3、transient 关键字,主要用来忽略我们不希望进行序列化的变量。
思考:怎么绕开 transient 机制的办法?
答案:可以的,需要定义一个私有方法来解决。 可以参考ArrayList中的方法
public class User implements Serializable {
private static final long serialVersionUID = 5645895533877005194L;
private String name;
private Integer age;
private transient String sex;
private void writeObject(java.io.ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeObject(sex);
}
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
sex = (String) s.readObject();
}
}
writeObject 和 readObject 原理:writeObject 和 readObject 是两个私有的方法,他们是什么时候被调用的呢?从运行结果来看,它确实被调用。而且他们并不存在于 Java.lang.Object,也没有在 Serializable 中去声明。我们唯一的猜想应该还是和 ObjectInputStream 和 ObjectOutputStream 有关系,所以基于这个入口去看看在哪个地方有调用。从源码层面来分析可以看到,readObject 是通过反射来调用的。
1.3序列化发展历程
随着分布式架构的发展、和微服务架构的普及,服务与服务之间的通信称为最基本的需求。这个时候,
我们不仅需要考虑通信的性能,也需要考虑到语言多元化问题。
所以,对于序列化来说,如何去提升序列化性能以及解决跨语言问题,就成了一个重点考虑的问题。
java本身序列化方式存在的问题:
1、序列化的数据比较大,传输效率低
2、其他语言无法识别和对接
因此在很长一段事件使用的是基于XML格式编码的对象序列化机制,XML序列化机制一方面可以解决跨语言的兼容问题,另一方面比二进制的序列化方式更容易理解。以至于基于XML的SOAP协议及对应的 WebService框架在很长一段时间内成为各个主流开发语言的必备的技术。
再到后来基于 JSON 的简单文本格式编码的 HTTP REST 接口又基本上取代了复杂的 WebService 接口,成为分布式架构中远程通信的首要选择。但是 JSON 序列化存储占用的空间大、性能低等问题,同时移动客户端应用需要更高效的传输数据来提升用户体验。在这种情况下与语言无关并且高效的二进制编码协议就成为了大家追求的热点技术之一。首先诞生的一个开源的二进制序列化框架-MessagePack。它比 google 的 Protocol Buffers 出现得还要早。
2、XML序列化框架介绍
XML 序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的字节码文件比较大,而且效率不高,适用于对性能不高,而且 QPS 较低的企业级内部系统之间的数据交换的场景,同时 XML 又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟知的 Webservice,就是采用 XML 格式对数据进行序列化的。XML 序列化/反序列化的实现方式有很多,熟知的方式有 XStream 和 Java 自带的 XML 序列化和反序列化两种。
Demo
com.thoughtworks.xstream
xstream
1.4.10
public class XStreamSerializer implements Serializer {
XStream xStream = new XStream(new DomDriver());
@Override
public byte[] serialize(T obj) {
return xStream.toXML(obj).getBytes();
}
@Override
public T deserialize(byte[] data, Class clazz) {
return (T) xStream.fromXML(new String(data));
}
}
3、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 非常容易使用 。(这个比较常用就不演示了)
Demo
4、Hessian 序列化框架
Hessian 是一个支持跨语言传输的二进制序列化协议,相对于 Java 默认的序列化机制来说,Hessian 具有更好的性能和易用性,而且支持多种不同的语言。
实际上 Dubbo 采用的就是 Hessian 序列化来实现,只不过 Dubbo 对 Hessian 进行了重构,性能更高。
hessian格式:User(name=kangkang, age=12, sex=null)
com.caucho
hessian
4.0.38
public class HessianSerializer implements Serializer {
@Override
public byte[] serialize(T obj) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(outputStream);
try {
hessianOutput.writeObject(obj);
return outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[0];
}
@Override
public T deserialize(byte[] data, Class clazz) {
ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
HessianInput hessianInput = new HessianInput(inputStream);
try {
return (T) hessianInput.readObject();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
5、Avro 序列化
Avro 是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷,快速地处理大量数据;
动态语言友好,Avro 提供的机制使动态语言可以方便地处理 Avro 数据。
6、kyro 序列化框架
Kryo 是一种非常成熟的序列化实现,已经在 Hive、Storm中使用得比较广泛,不过它不能跨语言. 目前 dubbo 已经在 2.6 版本支持 kyro 的序列化机制。它的性能要优于之前的hessian2。
7、Google Protobuf
7.1简介
在了解ProtoBuf之前,先了解下RPC远程调用服务,在RPC之前有RMI(Remote Method Invation)EJB时代的远程方法调用,有个很大的限制必须使用Java语言。后来使用RPC(Remote Proccedure Call)远程过程调用,很多RPC框架是跨语言【通过接口来定义一个说明文件,通过RPC框架提供的编译器将接口说明文件编译成具体的语言文件,在客户端与服务器端分别引入语言文件即可像调用本地方法一样掉用远程方法】 用于服务与服务之间的调用。
Protobuf是Google 的一种数据交换格式,它独立于语言、独立于平台。Google 提供了多种语言来实现,比如 Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件,Protobuf 是一个纯粹的表示层协议,可以和各种传输层协议一起使用。
Protobuf 使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的 RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中。
但是要使用 Protobuf 会相对来说麻烦些,因为他有自己的语法,有自己的编译器,如果需要用到的话必须要去投入成本在这个技术的学习中protobuf 有个缺点就是要传输的每一个类的结构都要生成对应的 proto 文件,如果某个类发生修改,还得重新生成该类对应的 proto 文件。
7.2Demo
gitHub上下载编译器 https://github.com/protocolbuffers/protobuf/releases protoc-3.14.0-win64.zip配置环境变量方便运行
编写一个proto文件
syntax="proto2";
package com.theory.demo._01SerializableDemo.dto;
option optimize_for = SPEED;
option java_package = "com.theory.demo._01SerializableDemo.dto";
option java_outer_classname="UserProto";
message User {
required string name=1;
required int32 age=2;
optional string sex=3;
}
通过命令生成java文件,protoc --java_out=./ ./user.proto (定位到proto所在的文件夹)
将生成的java文件引入到项目中,项目中引入POM
com.google.protobuf
protobuf-java
3.14.0
com.google.protobuf
protobuf-java-util
3.14.0
注意:jar包的版本号一定要和编译器的版本号一致否则可能会报错。
使用Demo:
public class ProtocolBufSerializer {
public byte[] serialize() {
UserProto.User user = UserProto.User.newBuilder()
.setAge(1)
.setSex("男")
.setName("test")
.build();
return user.toByteArray();
}
public UserProto.User deserialize(byte[] data) throws InvalidProtocolBufferException {
return UserProto.User.parseFrom(data);
}
}
7.3原理
案例:定义一个UserProtos.User user=UserProtos.User.newBuilder().setAge(300).setName("Mic").build();
protobuf输出的结果是10 3 77 105 99 16 -84 2
正常来说,要达到最小的序列化结果,一定会用到压缩的技术,而 protobuf 里面用到了两种压缩算法,一种是 varint,另一种是 zigzag。
varint方式
这两个字节字节分别的结果是:-84 、2
-84 怎么计算来的呢? 我们知道在二进制中表示负数的方法,高位设置为 1, 并且是对应数字的二进制取反以后再计算补码表示(补码是反码+1)所以如果要反过来计算
1. 【补码】10101100 -1 得到 10101011
2. 【反码】01010100 得到的结果为 84. 由于高位是 1,表示负数所以结果为-84
字符如何转化为编码
" Mic "这个字符,需要根据 ASCII 对照表转化为数字。
M =77、i=105、c=99
所以结果为 72 101 108 108 111
大家肯定有个疑问,这里的结果为什么直接就是 ASCII 编码的值呢?怎么没有做压缩呢?
原因是,varint 是对字节码做压缩,但是如果这个数字的二进制只需要一个字节表示的时候,其实最终编 码出来的结果是不会变化的还有两个数字,3 和 16 代表什么呢?那就要了解 protobuf 的存储格式了。
存储格式
protobuf 采用 T-L-V 作为存储方式
[图片上传失败...(image-21f1ce-1611632037043)]
结果:10 3 77 105 99 16 -84 2
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 3 77 105 99
10代表 字段编号和字段类型 规定是右移三位得到的 3代表往右移动3位
77 105 99 代表“Mic”对应的Ascii码
负数的存储
在计算机中,负数会被表示为很大的整数,因为计算机定义负数符号位为数字的最高位,所以如果采用 varint 编码表示一个负数,那么一定需要 5 个比特位。所以在 protobuf 中通过sint32/sint64 类型来表示负数,负数的处理形式是先采用 zigzag 编码(把符号数转化为无符号数),在采用 varint 编码。
sint32:(n << 1) ^ (n >> 31)
sint64:(n << 1) ^ (n >> 63)
比如存储一个(-300)的值
-300
原码:0001,0010,1100 【300】
取反: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
7. 4总结
Protocol Buffer 的性能好,主要体现在 序列化后的数据体积小 & 序列化速度快,最终使得传输效率高,其原因如下:
a. 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等)
b. 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成
序列化后的数据量体积小(即数据压缩效果好)的原因:
a. 采用了独特的编码方式,如 Varint、Zigzag 编码方式等等
b. 采用 T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑