前言:
昨天開始構思要寫一篇文章來說明一下序列化與反序列化。
因爲看到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框架,必定有一個優秀的對象序列化方式,所以,我們在選擇對象的序列化方式時,可以多考察下。
最後附上一個鏈接,比較常見的各種序列化方式(從時間和空間角度):https://github.com/eishay/jvm-serializers/wiki