前言:
昨天开始构思要写一篇文章来说明一下序列化与反序列化。
因为看到zookeeper的自定义序列化Jute,而且还自定义了通信协议。
之前感觉这些都我来说都是很牛逼的存在,从来没想过要做这些东西,但是仔细一想,好像也么有很难实现的地方吧,当然这都是后话,我们先一点点分析,先写一下序列化与反序列化的事情,后面再手撸一个RPC的调用。
1.为什么需要序列化?
如果面试的时候问到这个问题,那么每个人基本都有不同的见解,当然也会展示出每个人的技术深度。
笔者的理解是:我们正常创建出来的对象都在内存中(一般都存在于堆空间中),而内存中的内容往往是不可靠的,如果服务器发生宕机,断电之后,那么内存中的这些对象信息都会消失。
那如果我们还想继续用这些对象怎么办?
那么只能将这些对象信息存储在不易消失的地方,比如硬盘(这个存储方式就随意发挥了,可以是RDBMS,也可以是文件)
那么一个内存中的对象如何能保存在磁盘上的一个文件中呢?
这个时候就用上序列化了!
2.如何进行序列化?
实际这个问题就很简单了,分两个步骤:
1)将内存中的对象转换为字节数组
2)将字节数组存储到磁盘
3.序列化的方式?
这个问题算是对第二个问题的补充,我们知道如何进行序列化之后,那么序列化的方式有哪些呢?
回忆一下最常用的应该就是JDK自带的序列化了吧。那么我们通过一个示例来回忆一下。
当然在这之前,我们首先要有一个实体类,如下所示,后面我们要做的序列化主要针对这个类的相关对象来做的。
public class Student implements Serializable {
// 注意:这个实现序列化接口是必不可少的
private static final long serialVersionUID = -1967707976943843439L;
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student() {
}
// getter setter方法省略...
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
1)jdk序列化方式
public class JdkSerialize {
public static void main(String[] args) {
serialize();
}
private static void serialize() {
Student jack = new Student("jack", 22);
// 文件夹路径需要提前创建好
String jdkFilePath = "D:\\serialize\\jdk\\student2.txt";
try {
// 序列化到文件中
FileOutputStream fileOutputStream = new FileOutputStream(jdkFilePath);
ObjectOutputStream dataOutputStream = new ObjectOutputStream(fileOutputStream);
dataOutputStream.writeObject(jack);
// 反序列化
FileInputStream fileInputStream = new FileInputStream(jdkFilePath);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Student jackCopy = (Student) objectInputStream.readObject();
System.out.println(jackCopy);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
// res
Student{name='jack', age=22}
执行完上述序列化和反序列化方法后,我们可以看到在该文件路径生成的文件,有107字节。
总结:这种方式是jdk自带的序列化方式,当然也是我们平时常用的一种方式。
那么问题来了,这种jdk自带的方式是否就是最好的序列化方式呢?
思维发散:
那还有哪些方式可以将一个对象以字节的方式存储在文件中,然后还能从文件中重组出这个对象的全部信息呢?
我们可以想一下,如果我们先把这个对象变成一个字符串,然后再将字符串转换成字节是不是就可以了,这个对我们来说实现方式是很简单的。
有哪些方式可以将一个对象转换成字符串呢?最常用的就是Json,xml也可以,但是感觉xml比json长太多了,所以,我们就用json来实现下对象的序列化和反序列化来试试水
2)JSON序列化方式
笔者在这里选用FastJson来作为序列化工具,版本为1.2.58,具体如下:
public class JsoSerialize {
public static void main(String[] args) {
serialize();
}
private static void serialize() {
Student jack = new Student("jack", 22);
String path = "D:\\serialize\\json\\student.txt";
try {
// json序列化
ObjectOutputStream dataOutputStream = new ObjectOutputStream(new FileOutputStream(path));
String s = JSONObject.toJSONString(jack);
dataOutputStream.writeObject(s);
// json反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(path));
String o = (String)objectInputStream.readObject();
Student student = JSONObject.parseObject(o, Student.class);
System.out.println(student);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
// res
Student{name='jack', age=22}
与刚才我们使用JDK反序列的结果一样。
我们在其生成的文本中,可以看到只有24字节,比刚才jdk生成的字节更短了。
实际在我们的开发过程中,Json还是一种不错的方式来进行对象的序列化,那么还有没有更好的方式呢?请看下面一种方式。
3)Protobuf
在这里就不科普这种方式,具体可以在网上搜一下,一大把相关资料。笔者只是把这种方式推出来,看下它生成的字节能不能更短小精悍。
笔者使用proto3。
* 大家需要先安装一个proto包;
* 然后生成一下对应的.proto文件;
// student.proto
syntax = "proto3"; // PB协议版本
option java_package = "com.example.demo.serialize.protobuf";
option java_outer_classname="StudentProto";
message studentProtoTest {
string name = 1;
int32 age = 2;
}
* 然后根据protoc工具生成.proto文件对应的java类。
// 进入student.proto文件所在的目录,执行protoc命令,如下:
protoc -I=./ --java_out=./ ./student.proto
// res:会生成一个StudentProto.java类
* 根据生成的StudentProto.java类,进行序列化操作
public class ProtobufSerialize {
public static void main(String[] args) {
serialize();
}
private static void serialize() {
StudentProto.studentProtoTest.Builder builder = StudentProto.studentProtoTest.newBuilder();
builder.setName("lucy");
builder.setAge(22);
StudentProto.studentProtoTest build = builder.build();
// 序列化
byte[] bytes = build.toByteArray();
System.out.println("byte length: " + bytes.length);
// 反序列化
StudentProto.studentProtoTest studentTestResult = null;
try {
studentTestResult = StudentProto.studentProtoTest.parseFrom(bytes);
System.out.println(String.format("反序列化得到的信息,姓名:%s,性别:%d", studentTestResult.getName(), studentTestResult.getAge()));
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}
}
// res:
byte length: 8
反序列化得到的信息,姓名:lucy,性别:22
很令人的惊叹的结果,byte数组长度只有8,只有Json方式的三分之一,只有JDK方式的十二分之一。
可能会有人问了,短了又怎样呢,有啥好处嘛?
好处多多,我们同样的硬盘可以存储更多的对象;在网络传输时,我们同样的带宽可以传输更多的对象信息了,对企业来说都是巨大的成本,只有原成本的十分之一不到(JDK方式),就可以做同样的事情了。
总结:
写在最后了,一个好的RPC框架,必定有一个优秀的对象序列化方式,所以,我们在选择对象的序列化方式时,可以多考察下。