序列化框架的选型和比对
序列化通信
大白话介绍下 RPC 中序列化的概念,可以简单理解为对象 –> 字节的过程,同理,反序列化则是相反的过程。
为什么需要序列化? 因为网络传输只认字节。所以互信的过程依赖于序列化。
为什么需要序列化协议? 序列化之后数据格式是二进制字节流,那么从哪里开始的字节流到哪里结束是一个整型(或者其他类型),这就需要序列化协议来表示。
有人会问,FastJson 转换成字符串算不算序列化?对象持久化到数据库算不算序列化?没必要较真,广义上理解即可。
jdk序列化
jdk自身便带有序列化的功能,Java序列化API允许我们将一个对象转换为流,并通过网络发送,或将其存入文件或数据库以便未来使用,反序列化则是将对象流转换为实际程序中使用的Java对象的过程。
Java原生的序列化协议把字段类型信息用字符串格式写到了二进制流里面,这样反序列化方就可以根据字段信息来反序列化。但是Java原生的序列化协议最大的问题就是生成的字节流太大
示例
首先我们创建一个基础的测试Person类
import java.io.Serializable;
/**
* <p>ClassName: Person<p>
* <p>Description:测试对象序列化和反序列化<p>
*/
public class Person implements Serializable {
/**
* 序列化ID
*/
private static final long serialVersionUID = -5809782578272943999L;
private int age;
private String name;
private String sex;
public int getAge() {
return age;
}
public String getName() {
return name;
}
public String getSex() {
return sex;
}
public void setAge(int age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
'}';
}
}
通过jdk提供的序列化对其进行相应的序列化和反序列化的代码案例
public class TestObjSerializeAndDeserialize {
private static final String FILE_PATH =
"/Users/huangyuan/Desktop/Study/code/java-demo/src/main/java/huangy/serilize/Person.txt";
public static void main(String[] args) throws Exception {
// 序列化Person对象
SerializePerson();
// 反序列Perons对象
Person p = DeserializePerson();
System.out.println(p);
}
/**
* MethodName: SerializePerson
* Description: 序列化Person对象
*/
private static void SerializePerson() throws FileNotFoundException,
IOException {
Person person = new Person();
person.setName("gacl");
person.setAge(25);
person.setSex("男");
// ObjectOutputStream 对象输出流,将Person对象存储到Person.txt文件中,完成对Person对象的序列化操作
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(
new File(FILE_PATH)));
oo.writeObject(person);
System.out.println("Person对象序列化成功!");
oo.close();
}
/**
* MethodName: DeserializePerson
* Description: 反序列Perons对象
*/
private static Person DeserializePerson() throws Exception, IOException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
new File(FILE_PATH)));
Person person = (Person) ois.readObject();
System.out.println("Person对象反序列化成功!");
return person;
}
}
如果某些特殊字段不希望被序列化该如何处理?
这里面如果有相应的属性不希望被序列化操作的话,可以使用transient关键字进行修饰,例如希望tel属性不希望被序列化,可以改成这样:
private transient String tel;
这样的话,该对象在反序列化出来结果之后,相应的属性就会为null值。
为什么要定义serialVersionUID?
序列化操作时,系统会把当前类声明的serialVersionUID写入到序列化字节流中,用于反序列化时系统会去检测序列化字节流中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。
如果没有定义serialVersionUID时
当实现当前类没有显式地定义一个serialVersionUID变量时候,Java序列化机制会根据编译的Class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,如果对类信息进行修改,会导致反序列化时serialVersionUID与原先值无法匹配,反序列化失败。
优点
简单、易用
缺点
- 无法跨语言
- 对于跨进程的服务调用,通常都需要考虑到不同语言的相互调用时候的兼容性,而这一点对于jdk序列化操作来说却无法做到。
- 这是因为jdk序列化操作时是使用了java语言内部的私有协议,在对其他语言进行反序列化的时候会有严重的阻碍。
- 序列化之后的字节数组过大
- jdk序列化之后产生的字节数组过大,占用的内存空间也较高,这就导致了相应的流在网络传输的时候带宽占用较高,性能相比较为低下的情况。
- jdk序列化之后产生的字节数组过大,占用的内存空间也较高,这就导致了相应的流在网络传输的时候带宽占用较高,性能相比较为低下的情况。
Hessian
Hessian是一款跨语言进行序列化操作的框架技术,同时在进行序列化之后产生的码流也较小,处理数据的性能方面远超于java内置的jdk序列化方式。
示例
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class HessianTest {
public static void main(String[] args) throws IOException {
Person person = new Person();
person.setAge(1);
person.setName("idea");
person.setSex("man");
// Hessian序列化对象
ByteArrayOutputStream os = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(os);
ho.writeObject(person);
byte[] userByte = os.toByteArray();
// Hessian的反序列化对象
ByteArrayInputStream is = new ByteArrayInputStream(userByte);
HessianInput hi = new HessianInput(is);
Person newPerson = (Person) hi.readObject();
System.out.println(newPerson);
}
}
原理
在字节流中不存放字段类型信息,仅仅是key-value的形式,因此字节流相对于jdk序列化会小很多。
Hessian 会把复杂对象所有属性存储在一个 Map 进行序列化。所以在父类、子类存在同名成员变量的情况下, Hessian 序列化时,先序列化子类 ,然后序列化父类,因此反序列化结果会导致子类同名成员变量被父类的值覆盖。
相比 Hessian 1.0,Hessian 2.0 中增加了压缩编码,其序列化二进制流大小是 Java序列化的 50% , 序列化耗时是 Java 序列化的 30% ,反序列化耗时是 Java 反序列化的20% 。
优点
支持跨语言,序列化后字节数适中,API 易用。
是国内主流 rpc 框架:dubbo,motan 的默认序列化协议。
缺点
父类、子类存在同名成员变量的情况下,先序列化子类 ,然后序列化父类,因此反序列化结果会导致子类同名成员变量被父类的值覆盖。
Protocol Buffer
google protobuf是一个灵活的、高效的用于序列化数据的协议。相比较XML和JSON格式,protobuf更小、更快、更便捷。google protobuf是跨语言的,并且自带了一个编译器(protoc),只需要用它进行编译,可以编译成Java、python、C++、C#、Go等代码,然后就可以直接使用,不需要再写其他代码,自带有解析的代码。
protobuf相对于kryo来说具有更加高效的性能和灵活性,能够在实际使用中,当对象序列化之后新增了字段,在反序列化出来的时候依旧可以正常使用。
详细见 “ProtocolBuffer” 文章