天天看點

不可不知的 Java 序列化 | 技術創作101訓練營不可不知的 Java 序列化 | 技術創作101訓練營

不可不知的 Java 序列化 | 技術創作101訓練營

不可不知的 Java 序列化 | 技術創作101訓練營不可不知的 Java 序列化 | 技術創作101訓練營

前言

在程式運作的生命周期中,序列化與反序列化的操作,幾乎無時無刻不在發生着。對于任何一門語言來說,不管它是編譯型還是解釋型,隻要它需要通訊或者持久化時,就必然涉及到序列化與反序列化操作。但是,又正因為序列化與反序列化太過重要,太過普遍,大部分程式設計語言和架構都對其進行了很好的封裝,又因為他的潤物細無聲,使得我們很多時候根本沒有意識到,代碼下面其實進行了許許多多序列化相關的操作。今天我們就一起去探尋這位最熟悉的陌生人。

序列化是什麼

百度百科中給序列化的定義是『序列化 (Serialization)是将對象的狀态資訊轉換為可以存儲或傳輸的形式的過程。』。似乎有點抽象,下面用一個例子簡單類比一下。

日常生活中,總少不了人跟人之間的交流與溝通。而溝通的前提是先要把我們大腦中想的内容,通過某種形式表達出來。然後别人再通過我們表達出的内容去了解。

不可不知的 Java 序列化 | 技術創作101訓練營不可不知的 Java 序列化 | 技術創作101訓練營

而表達的方式多種多樣,最常見的就是說話,我們通過說一些話,把我們腦海裡想的内容表達出來,對方聽了這些話立刻明白了我們的想法。當然表達也可以是文字,比如你正在看的本文,不也是在與你交流嗎?導演通過電影去表達自己對于世界的了解,畫家通過畫作述說的對美的渴望,音樂家通過樂符描述着對自由的向往。凡此種種,不勝枚舉。

是以,這些又跟我們的主題 序列化 有什麼關系呢?

其實人與人之間少不了溝通交流,程式與程式之間,機器與機器之間也少不了溝通交流。隻不過通常不會說是溝通,我們會說請求、響應、傳輸、通訊…… 同樣的内容隻是換了一種說法。

上文中提到,人與人之間的溝通需要一種表達方式。通過這種表達方式把我們大腦中所想的内容,轉化成他人可以了解的内容。而機器與機器之間的通訊也需要這樣一種表達方式,通過這種表達方式把記憶體中的内容,轉化成其它機器可以讀取的内容。

不可不知的 Java 序列化 | 技術創作101訓練營不可不知的 Java 序列化 | 技術創作101訓練營

是以序列化可以簡單的了解成是 機器記憶體中資訊的表達方式 。

為什麼需要序列化

通常情況下,我們的語言一方面用于交流,比如聊天,把我腦海中的思想,通過語言表達出來,對方聽到我們的話語,會意我們的想法。

另一方面,我們的語言除了用于溝通交流,還可以用于記錄。有一句話叫做『好記性不如爛筆頭』。說的就是記錄的重要性,因為話在我們的腦子裡,很容易就忘了,通過記錄下來可以儲存更久。

而序列化功能又正好對應這兩點,一個是用來傳輸資訊,另一個是用來持久化。序列化用來傳輸的作用,前文已經說過了,關于持久化的作用,也很好了解。首先明确一個問題,序列化的是什麼内容?通常是記憶體中的内容。而記憶體有一個特點我們都知道,那就是一重新開機就沒了。對于部分内容,我們想在重新開機後還存在(比如說 tomcat 中 session 裡面的對象),要怎麼辦呢?答案就是把記憶體中的對象儲存到磁盤上,這樣就不怕重新開機了,而持久化就需要用到序列化技術。

如何實作序列化

人與人之間有許許多多的表達方式,而且機器與機器之間也同樣,序列化的方式多種多樣。

Java 原生形式

對于如此普遍的序列化需求,Java 其實早在 JDK 1.1 開始就在語言層面進行了支援。而且使用起來非常友善,下面我們就一起看看具體代碼。

  1. 首先我們要把想序列化的類實作 Java 自帶的

    java.io.Serializable

    接口
/*
 *
 *  * *
 *  *  * blog.coder4j.cn
 *  *  * Copyright (C) 2016-2020 All Rights Reserved.
 *  *
 *
 */
package cn.coder4j.study.example.serialization;

import java.io.Serializable;
import java.util.StringJoiner;

/**
 * @author buhao
 * @version HaveSerialization.java, v 0.1 2020-09-17 16:58 buhao
 */
public class HaveSerialization implements Serializable {
    private static final long serialVersionUID = -4504407589319471384L;
    private String name;
    private Integer age;

    /**
     * Getter method for property <tt>name</tt>.
     *
     * @return property value of name
     */
    public String getName() {
        return name;
    }

    /**
     * Setter method for property <tt>name</tt>.
     *
     * @param name value to be assigned to property name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Getter method for property <tt>age</tt>.
     *
     * @return property value of age
     */
    public Integer getAge() {
        return age;
    }

    /**
     * Setter method for property <tt>age</tt>.
     *
     * @param age value to be assigned to property age
     */
    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return new StringJoiner(", ", HaveSerialization.class.getSimpleName() + "[", "]")
                .add("name='" + name + "'")
                .add("age=" + age)
                .toString();
    }
}           

複制

需要注意的是,雖說是實作了

java.io.Serializable

接口,但是我們其實沒有覆寫任何方法。這是為什麼呢?我們一起看一下

java.io.Serializable

的源碼。

public interface Serializable {
}           

複制

沒錯,是個空接口,除了接口定義部分,啥也沒有。通常遇到這種情況,我們稱之為标記接口,主要為了标記某些類,标記的原因是,把它與其它類差別出來,友善我們後面專門處理。而  Serializable 這個标記接口,就是為了讓我們知道這個類是要進行序列化操作的類,僅此而已。

另外,雖然我們隻實作一個空接口,但是細心的你,肯定發現了我們的類中多了一個 serialVersionUID 屬性。那麼這個屬性的作用是什麼呢?

它主要目的就是為了驗證序列化與反序列化的類是否一緻。比如上面

HaveSerialization

這個類現在有業務屬性

name

age

,現在因為業務需要,我們要添加一個

address

的屬性。序列化操作是沒有問題的,但是把序列化資訊傳輸給其它機器,其它機器在反序列化的時候,就出現了問題。因為其它機器的

HaveSerialization

沒有

address

這個屬性。

為了解決這個問題,JDK 通過使用 serialVersionUID 在作為該類的版本号,在反序列化時比較傳輸的類的值與要反序列化類的值是否一緻,不一緻就會報 InvalidCastException 。

當然,出發點是好的,但是直接抛異常會導緻業務無法進行下去,通常 serialVersionUID 生成好後,我們不會再更新,序列化如果沒有更新,對應變更的屬性會為空,我們隻要在業務裡做好相容就好了。

  1. 序列化對象

好了,我們已經完成了第一步,定義了一個序列化類,下面我們就把他給序列化掉。

/**
     * 序列化對象(儲存序列化檔案)
     * @throws IOException
     */
    @Test
    public void testSaveSerializationObject() throws IOException {
        // 建立對象
        final HaveSerialization haveSerialization = new HaveSerialization();
        haveSerialization.setName("kiwi");
        haveSerialization.setAge(18);
        // 建立序列化對象儲存的檔案
        final File file = new File("haveSerialization.ser");
        // 建立對象輸出流
        try (final ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file))) {
            // 将對象輸出到序列化檔案
            objectOutputStream.writeObject(haveSerialization);
        }
    }           

複制

可以看到代碼十分簡單,大體分成如下 4 步:

  1. 建立要序列化的對象

    其實就是你上面的實作

    java.io.Serializable

    的類,如果沒有實作,在這裡會報

    NotSerializableException

    異常
  2. 建立一個

    File

    對象,用來儲存序列化後的二進制資料。

    注意這裡檔案名我用的是

    *.ser

    ,這個 ser 字尾并沒有強制要求,隻是友善了解,你可能寫成其它字尾
  3. 建立對象輸出流

    建立一個

    ObjectOutputStream

    對象輸出流的對象,并把上面定義的序列化檔案對象通過構造函數傳給它。
  4. 通過輸出流把對象寫到序列化檔案裡

    注意這裡我用的 JDK 8 的

    try with resource

    文法,是以不用手動

    close

好了,到這裡我們序列化也完成了。

  1. 反序列化對象

既然有序列化,那肯定也有反序列化。反序列化可以了解成是序列化的逆向操作,既然序列化把記憶體中的對象轉成一個可以持久化的檔案,那麼反序列化要做的就是把這個檔案再加載到記憶體中的對象。話不多說,直接看代碼。

/**
     * 反序列化對象(從序列化檔案中讀取對象)
     * @throws IOException
     * @throws ClassNotFoundException
     */
    @Test
    public void testLoadSerializationObject() throws IOException, ClassNotFoundException {
        // 建立對象輸出流
        try (ObjectInputStream objectInputStream = new ObjectInputStream(
                new FileInputStream(new File("haveSerialization.ser")))) {
            // 從輸出流中建立對象
            final Object obj = objectInputStream.readObject();
            System.out.println(obj);
        }
    }           

複制

是的,反序列化代碼比序列化代碼還少,主要分成如下 2 步:

  1. 建立對象輸入流

    建立一個

    ObjectInputStream

    對象,并把序列化檔案通過構造函數傳給它
  2. 從對象輸入流中讀取對象

    直接通過

    readObject

    方法即可,注意讀取後是

    Object

    類型,後續使用需手動強轉一次

到這裡,我們便通過 JDK 原生的方法完成了序列化與反序列化操作,是不是還很簡單。但是日常工作不太推薦直接使用原生的方式實作序列化,一方面它生成的序列化檔案較大,一方面也比一些第三方架構生成的慢,但是序列化原理大緻類似。下面我們簡單看一下其它方式如何序列化。

通用對象序列化

通常序列化是與語言綁定的,比如說通過上面 JDK 序列化的檔案,不可能拿給 PHP 應用反序列化成 PHP 的對象。不過可以通過某些特殊的通用對象結構序列化來實作跨語言使用,比較常見的是 JSON 、XML 。下面我們以 JSON 為例看一下

/**
     * 測試序列化通過json
     */
    @Test
    public void testSerializationByJSON(){
        //-------------序列化操作---------------
        // 建立對象
        final HaveSerialization haveSerialization = new HaveSerialization();
        haveSerialization.setName("kiwi");
        haveSerialization.setAge(18);

        // 序列化成 JSON 字元串
        final String jsonString = JSON.toJSONString(haveSerialization);
        System.out.println("JSON:" + jsonString);

        //-------------反序列化操作---------------
        final HaveSerialization haveSerializationByJSON = JSON.parseObject(jsonString, HaveSerialization.class);
        System.out.println(haveSerializationByJSON);
    }           

複制

運作結果:

JSON:{"age":18,"name":"kiwi"}
HaveSerialization[name='kiwi', age=18]           

複制

上述代碼使用的 JSON 架構是 alibaba/fastjson 。但是大部分 JSON 架構使用起來都大同小異。可以按個人喜好去替換。

序列化架構

序列化架構其實有很多,比如

kryo

hessian

protostuff

。它們各有優缺點,詳細的比較可以看這篇文章 序列化架構 kryo VS hessian VS Protostuff VS java 。大家可以按各自的使用場景選擇使用,下文以

kryo

為例示範。

  1. 依賴
<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>5.0.0-RC9</version>
</dependency>           

複制

  1. 具體代碼
/**
     * 測試序列化通過kryo
     */
    @Test
    public void testSerializationByKryo() throws FileNotFoundException {
        //-------------序列化操作---------------
        // 建立對象
        final HaveSerialization haveSerialization = new HaveSerialization();
        haveSerialization.setName("kiwi");
        haveSerialization.setAge(18);

        final Kryo kryo = new Kryo();
        // 注冊序列化類
        kryo.register(HaveSerialization.class);

        // 序列化操作
        try (final Output output = new Output(new FileOutputStream("haveSerialization.kryo"))) {
            kryo.writeObject(output, haveSerialization);
        }

        // 反序列化
        try (final Input input = new Input(new FileInputStream("haveSerialization.kryo"))) {
            final HaveSerialization haveSerializationByKryo = kryo.readObject(input, HaveSerialization.class);
            System.out.println(haveSerializationByKryo);
        }
    }           

複制

其實看代碼可以發現跟 JDK 的流程幾乎一樣,其中有幾點需要注意的,

kryo

在序列化前,要手動通過

register

注冊序列化的類,有點類似 JDK 實作

java.io.Serializable

接口。然後

Input

Output

對象不是 JDK 的。是

kryo

提供的。另外

Kryo

有不少需要注意的地方,可以檢視參考連結部分的内容學習。

源碼位址

因文章篇幅有限,無法展示所有代碼,已經另外把完整代碼上傳到 github,具體連結如下:

https://github.com/kiwiflydream/study-example/tree/master/study-serialization-example

參考連結

  1. 序列化架構 kryo VS hessian VS Protostuff VS java - 知其然,知其是以然 - ITeye部落格
  2. Kryo 使用指南 - hntyzgn - 部落格園
  3. 深入了解 RPC 之序列化篇 --Kryo | 徐靖峰|個人部落格
  4. EsotericSoftware/kryo: Java binary serialization and cloning: fast, efficient, automatic

總結

本文主要介紹了 Java 序列化的相關内容,主要介紹序列化是什麼?與人與人之間溝通的表達方式做類比,得到是 機器記憶體中資訊的表達方式 。而為什麼需要序列化,我們通過舉例說明了序列化 資訊傳輸與持久化 的功能。最後我們一起從 JDK 原生的實作 java.io.Serializable 的方式,再到通用對象序列化的 JSON、XML 方式,最終到第三方架構 kryo 的形式了解如何去實作序列化。