序列化與反序列化的那些事

前言:

    昨天開始構思要寫一篇文章來說明一下序列化與反序列化。

    因爲看到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  

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章