分布式四、序列化與反序列化

Valentine 轉(zhuǎn)載請(qǐng)標(biāo)明出處。

序列化的意義

實(shí)體在網(wǎng)絡(luò)中通信

Java 平臺(tái)允許我們?cè)趦?nèi)存中創(chuàng)建可復(fù)用的Java 對(duì)象,但一般情況下,只有當(dāng)JVM 處于運(yùn)行時(shí),這些對(duì)象才可能存在,即,這些對(duì)象的生命周期不會(huì)比JVM 的生命周期更長(zhǎng)。但在現(xiàn)實(shí)應(yīng)用中,就可能要求在JVM停止運(yùn)行之后還能夠保存(持久化)指定的對(duì)象,并在將來(lái)重新讀取被保存的對(duì)象,Java 對(duì)象的序列化就能夠?qū)崿F(xiàn)該功能。
簡(jiǎn)單來(lái)說(shuō)
序列化是把對(duì)象的狀態(tài)信息轉(zhuǎn)化為可存儲(chǔ)或傳輸?shù)男问竭^(guò)程,就是把對(duì)象轉(zhuǎn)化為字節(jié)序列的稱為對(duì)象的序列化,反序列化是序列化的逆向過(guò)程,把字節(jié)序列恢復(fù)為對(duì)象的過(guò)程稱為對(duì)象的反序列化。

序列化面臨的挑戰(zhàn)

評(píng)價(jià)一個(gè)序列化算法優(yōu)劣的兩個(gè)重要指標(biāo)是:
1、序列化以后的數(shù)據(jù)大??;
2、序列化操作本身的速度及系統(tǒng)資源開(kāi)銷(CPU、內(nèi)存);
Java 語(yǔ)言本身提供了對(duì)象序列化機(jī)制,也是Java 語(yǔ)言本身最重要的底層機(jī)制之一,但是Java 本身提供的序列化機(jī)制存在兩個(gè)問(wèn)題:

  1. 序列化的數(shù)據(jù)比較大,傳輸效率低
  2. 其他語(yǔ)言無(wú)法識(shí)別和對(duì)接
    如何實(shí)現(xiàn)一個(gè)序列化操作
    在Java 中,只要一個(gè)類實(shí)現(xiàn)了java.io.Serializable 接口,那么它就可以被序列化
    定義接口
public interface ISerializer {
    // 序列化
    <T> byte[] serializer(T obj);
    // 反序列化
    <T> T deSerializer(byte[] data,Class<T> clazz);
}

public class JavaSerializer implements ISerializer {

    @Override
    public <T> byte[] serializer(T obj) {
        ObjectOutputStream objectOutputStream=null;
        try {
            objectOutputStream=new ObjectOutputStream(new FileOutputStream(new File("user")));
            objectOutputStream.writeObject(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(objectOutputStream!=null){
                try {
                    objectOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    @Override
    public <T> T deSerializer(byte[] data, Class<T> clazz) {
        ObjectInputStream objectInputStream=null;
        try {
            objectInputStream=new ObjectInputStream(new FileInputStream(new File("user")));
            return (T)objectInputStream.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if(objectInputStream!=null){
                try {
                    objectInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

基于JDK 序列化方式實(shí)現(xiàn)
JDK 提供了Java 對(duì)象的序列化方式, 主要通過(guò)輸出流java.io.ObjectOutputStream 和對(duì)象輸入流java.io.ObjectInputStream來(lái)實(shí)現(xiàn)。其中,被序列化的對(duì)象需要實(shí)現(xiàn)java.io.Serializable 接口。

public class SuperClass implements Serializable {

    String sex;
    ...
}

public class User extends SuperClass {

    public static int num=5;

    private String name;

    private int age;

    private transient String hobby;

    //序列化對(duì)象
    private void writeObject(ObjectOutputStream objectOutputStream) throws IOException {
        objectOutputStream.defaultWriteObject();
        objectOutputStream.writeObject(hobby);
    }

    //反序列化
    private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
        objectInputStream.defaultReadObject();
        hobby=(String)objectInputStream.readObject();
    }
     ...
}

public static void main(String[] args) {
        JavaSerializer javaSerializer = new JavaSerializer();
        User user = new User();
        user.setAge(18);
        user.setName("Valentine");
        user.setHobby("健身");
        User user1 = iSerializer.deSerializer(null, User.class);
    }

序列化的高階認(rèn)識(shí)

serialVersionUID 的作用,Java 的序列化機(jī)制是通過(guò)判斷類的serialVersionUID 來(lái)驗(yàn)證版本一致性的。在進(jìn)行反序列化時(shí),JVM 會(huì)把傳來(lái)的字節(jié)流中的serialVersionUID與本地相應(yīng)實(shí)體類的serialVersionUID 進(jìn)行比較,如果相同就認(rèn)為是一致的,可以進(jìn)行反序列化,否則就會(huì)出現(xiàn)序列化版本不一致的異常,即是InvalidCastException如果沒(méi)有為指定的class 配置serialVersionUID,那么java 編譯器會(huì)自
動(dòng)給這個(gè)class 進(jìn)行一個(gè)摘要算法,類似于指紋算法,只要這個(gè)文件有任何改動(dòng),得到的UID 就會(huì)截然不同的,可以保證在這么多類中,這個(gè)編號(hào)是唯一的。
serialVersionUID 有兩種顯示的生成方式:
一是默認(rèn)的1L,比如:private static final long serialVersionUID = 1L;
二是根據(jù)類名、接口名、成員方法及屬性等來(lái)生成一個(gè)64 位的哈希字段;
當(dāng)實(shí)現(xiàn)java.io.Serializable 接口的類沒(méi)有顯式地定義一個(gè)serialVersionUID 變量時(shí)候,Java 序列化機(jī)制會(huì)根據(jù)編譯的Class 自動(dòng)生成一個(gè)serialVersionUID 作序列化版本比較用,這種情況下,如果Class 文件(類名,方法明等)沒(méi)有發(fā)生變化(增加空格,換行,增加注釋等
等),就算再編譯多次,serialVersionUID 也不會(huì)變化的。

靜態(tài)變量序列化

public class App {
    public static void main(String[] args) {
        JavaSerializer javaSerializer = new JavaSerializer();
        User user = new User();
        user.setAge(18);
        user.setName("Valentine");
        user.setHobby("健身");
        user.num = 10;
        User user1 = iSerializer.deSerializer(null, User.class);
        System.out.println(user1.num);
    }
}

在User 中添加一個(gè)全局的靜態(tài)變量num , 在執(zhí)行序列化以后修改num 的值為10, 然后通過(guò)反序列化以后得到的對(duì)象去輸出num 的值,然后通過(guò)反序列化以后得到的對(duì)象去輸出num 的值最后的輸出是 10,理論上打印的 num 是從讀取的對(duì)象里獲得的,應(yīng)該是保存時(shí)的狀態(tài)才對(duì)。之所以打印 10 的原因在于序列化時(shí),并不保存靜態(tài)變量,這其實(shí)比較容易理解,序列化保存的是對(duì)象的狀態(tài),靜態(tài)變量屬于類的狀態(tài),因此 序列化并不保存靜態(tài)變量。

父類的序列化

一個(gè)子類實(shí)現(xiàn)了 Serializable 接口,它的父類都沒(méi)有實(shí)現(xiàn) Serializable接口,在子類中設(shè)置父類的成員變量的值,接著序列化該子類對(duì)象。再反序列化出來(lái)以后輸出父類屬性的值。結(jié)果應(yīng)該是什么?
發(fā)現(xiàn)父類的字段的值為null。也就是父類沒(méi)有實(shí)現(xiàn)序列化。
結(jié)論:

  1. 當(dāng)一個(gè)父類沒(méi)有實(shí)現(xiàn)序列化時(shí),子類繼承該父類并且實(shí)現(xiàn)了序列化。在反序列化該子類后,是沒(méi)辦法獲取到父類的屬性值的;
  2. 當(dāng)一個(gè)父類實(shí)現(xiàn)序列化,子類自動(dòng)實(shí)現(xiàn)序列化,不需要再顯示實(shí)現(xiàn)Serializable 接口;
  3. 當(dāng)一個(gè)對(duì)象的實(shí)例變量引用了其他對(duì)象,序列化該對(duì)象時(shí)也會(huì)把引用對(duì)象進(jìn)行序列化,但是前提是該引用對(duì)象必須實(shí)現(xiàn)序列化接口;

Transient 關(guān)鍵字

Transient 關(guān)鍵字的作用是控制變量的序列化,在變量聲明前加上該關(guān)鍵字,可以阻止該變量被序列化到文件中,在被反序列化后,transient變量的值被設(shè)為初始值,如 int 型的是 0,對(duì)象型的是 null。

繞開(kāi)transient 機(jī)制的辦法

writeObject和readObject 這兩個(gè)私有的方法,既不屬于Object、也不是Serializable,為什么能夠在序列化的時(shí)候被調(diào)用呢?
原因是,ObjectOutputStream使用了反射來(lái)尋找是否聲明了這兩個(gè)方法。因?yàn)镺bjectOutputStream使用getPrivateMethod,所以這些方法必須聲明為private 以至于供ObjectOutputStream 來(lái)使用。

序列化的存儲(chǔ)規(guī)則

public class StoreRuleDemo {

    public static void main(String[] args) throws IOException {
        ObjectOutputStream outputStream =
                new ObjectOutputStream(new FileOutputStream(new File("user")));
        User user = new User();
        user.setAge(18);
        user.setName("Valentine");
        user.setHobby("健身");
        user.setSex("男");
        outputStream.flush();
        outputStream.writeObject(user);
        System.out.println(new File("user").length());
        outputStream.writeObject(user);
        outputStream.flush();
        outputStream.close();
        System.out.println(new File("user").length());
    }
}

同一對(duì)象兩次(開(kāi)始寫(xiě)入文件到最終關(guān)閉流這個(gè)過(guò)程算一次,上面的演示效果是不關(guān)閉流的情況才能演示出效果)寫(xiě)入文件,打印出寫(xiě)入一次對(duì)象后的存儲(chǔ)大小和寫(xiě)入兩次后的存儲(chǔ)大小,第二次寫(xiě)入對(duì)象時(shí)文件只增加了 5 字節(jié)。
Java 序列化機(jī)制為了節(jié)省磁盤(pán)空間,具有特定的存儲(chǔ)規(guī)則,當(dāng)寫(xiě)入文件的為同一對(duì)象時(shí),并不會(huì)再將對(duì)象的內(nèi)容進(jìn)行存儲(chǔ),而只是再次存儲(chǔ)一份引用,上面增加的 5 字節(jié)的存儲(chǔ)空間就是新增引用和一些控制信息的空間。反序列化時(shí),恢復(fù)引用關(guān)系,該存儲(chǔ)規(guī)則極大的節(jié)省了存儲(chǔ)空間。

序列化實(shí)現(xiàn)深克隆

在Java 中存在一個(gè)Cloneable 接口,通過(guò)實(shí)現(xiàn)這個(gè)接口的類都會(huì)具備clone 的能力,同時(shí)clone 是在內(nèi)存中進(jìn)行,在性能方面會(huì)比直接通過(guò)new生成對(duì)象要高一些,特別是一些大的對(duì)象的生成,性能提升相對(duì)比較明顯。那么在Java 領(lǐng)域中,克隆分為深度克隆和淺克隆。
淺克隆
被復(fù)制對(duì)象的所有變量都含有與原來(lái)的對(duì)象相同的值,而所有的對(duì)其他對(duì)象的引用仍然指向原來(lái)的對(duì)象。

public class Email implements Serializable {

    private String content;
    ...
}

public class Person implements Cloneable, Serializable {

    private String name;

    private Email email;
    ...

    @Override
    protected Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }

    public Person deepClone() throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream =
                new ObjectOutputStream(bos);
        objectOutputStream.writeObject(this);

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(bis);
        return (Person) objectInputStream.readObject();

    }
}

public class CloneDemo {

    public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
        Email email=new Email();
        email.setContent("八點(diǎn)回家");
        Person p1=new Person("Valentine");
        p1.setEmail(email);

//        Person p2=p1.clone();
        Person p2=p1.deepClone();
        p2.setName("Sam");
        p2.getEmail().setContent("9點(diǎn)回家");

        System.out.println(p1.getName()+"->"+p1.getEmail().getContent());
        System.out.println(p2.getName()+"->"+p2.getEmail().getContent());

    }
}

深克隆
被復(fù)制對(duì)象的所有變量都含有與原來(lái)的對(duì)象相同的值,除去那些引用其他對(duì)象的變量。那些引用其他對(duì)象的變量將指向被復(fù)制過(guò)的新對(duì)象,而不再是原有的那些被引用的對(duì)象。換言之,深拷貝把要復(fù)制的對(duì)象所引用的對(duì)象都復(fù)制了一遍。
實(shí)現(xiàn)深克隆效果的原理是把對(duì)象序列化輸出到一個(gè)流中,然后在把對(duì)象從序列化流中讀取出來(lái),這個(gè)對(duì)象就不是原來(lái)的對(duì)象了。

常見(jiàn)的序列化技術(shù)

使用JAVA 進(jìn)行序列化有他的優(yōu)點(diǎn),也有他的缺點(diǎn):
優(yōu)點(diǎn):JAVA 語(yǔ)言本身提供,使用比較方便和簡(jiǎn)單;
缺點(diǎn):不支持跨語(yǔ)言處理、 性能相對(duì)不是很好,序列化以后產(chǎn)生的數(shù)據(jù)相對(duì)較大;

XML 序列化框架

XML 序列化的好處在于可讀性好,方便閱讀和調(diào)試。但是序列化以后的字節(jié)碼文件比較大,而且效率不高,適用于對(duì)性能不高,而且QPS 較低的企業(yè)級(jí)內(nèi)部系統(tǒng)之間的數(shù)據(jù)交換的場(chǎng)景,同時(shí)XML 又具有語(yǔ)言無(wú)關(guān)性,所以還可以用于異構(gòu)系統(tǒng)之間的數(shù)據(jù)交換和協(xié)議。比如我們熟知的Webservice,就是采用XML 格式對(duì)數(shù)據(jù)進(jìn)行序列化的。

JSON 序列化框架

JSON(JavaScript Object Notation)是一種輕量級(jí)的數(shù)據(jù)交換格式,相對(duì)于XML 來(lái)說(shuō),JSON 的字節(jié)流更小,而且可讀性也非常好?,F(xiàn)在JSON數(shù)據(jù)格式在企業(yè)運(yùn)用是最普遍的JSON 序列化常用的開(kāi)源工具有很多:
1、 Jackson (https://github.com/FasterXML/jackson
2、阿里開(kāi)源的FastJson (https://github.com/alibaba/fastjon
3、Google 的GSON (https://github.com/google/gson)
這幾種json 序列化工具中,Jackson與fastjson要比GSON的性能要好,但是Jackson、GSON 的穩(wěn)定性要比Fastjson 好。而Fastjson的優(yōu)勢(shì)在于提供的api 非常容易使用。

Hessian 序列化框架

Hessian 是一個(gè)支持跨語(yǔ)言傳輸?shù)亩M(jìn)制序列化協(xié)議,相對(duì)于Java 默認(rèn)的序列化機(jī)制來(lái)說(shuō),Hessian 具有更好的性能和易用性,而且支持多種不同的語(yǔ)言實(shí)際上Dubbo 采用的就是Hessian 序列化來(lái)實(shí)現(xiàn),只不過(guò)Dubbo 對(duì)Hessian 進(jìn)行了重構(gòu),性能更高。

Protobuf 序列化框架

Protobuf 是Google 的一種數(shù)據(jù)交換格式,它獨(dú)立于語(yǔ)言、獨(dú)立于平臺(tái)。Google 提供了多種語(yǔ)言來(lái)實(shí)現(xiàn),比如Java、C、Go、Python,每一種實(shí)現(xiàn)都包含了相應(yīng)語(yǔ)言的編譯器和庫(kù)文件Protobuf 使用比較廣泛,主要是空間開(kāi)銷小和性能比較好,非常適合用于公司內(nèi)部對(duì)性能要求高的RPC 調(diào)用。 另外由于解析性能比較高,序列化以后數(shù)據(jù)量相對(duì)較少,所以也可以應(yīng)用在對(duì)象的持久化場(chǎng)景中但是但是要使Protobuf 會(huì)相對(duì)來(lái)說(shuō)麻煩些,因?yàn)樗凶约旱恼Z(yǔ)法,有自己的編譯器。
總結(jié)
Protocol Buffer 的性能好,主要體現(xiàn)在 序列化后的數(shù)據(jù)體積小 & 序列化速度快,最終使得傳輸效率高,其原因如下:
序列化速度快的原因:
1、 編碼 / 解碼 方式簡(jiǎn)單(只需要簡(jiǎn)單的數(shù)學(xué)運(yùn)算 = 位移等等);
2、采用 Protocol Buffer 自身的框架代碼 和 編譯器 共同完成序列化后的數(shù)據(jù)量體積?。磾?shù)據(jù)壓縮效果好)的原因:
a. 采用了獨(dú)特的編碼方式,如Varint、Zigzag 編碼方式等等;
b. 采用T - L - V 的數(shù)據(jù)存儲(chǔ)方式:減少了分隔符的使用 & 數(shù)據(jù)存儲(chǔ)得緊湊;
各個(gè)序列化技術(shù)的性能比較,這個(gè)地址有針對(duì)不同序列化技術(shù)進(jìn)行性能比較:
https://github.com/eishay/jvm-serializers/wiki

序列化技術(shù)的選型

技術(shù)層面

1、序列化空間開(kāi)銷,也就是序列化產(chǎn)生的結(jié)果大小,這個(gè)影響到傳輸?shù)男阅堋?br> 2、序列化過(guò)程中消耗的時(shí)長(zhǎng),序列化消耗時(shí)間過(guò)長(zhǎng)影響到業(yè)務(wù)的響應(yīng)時(shí)間。
3、序列化協(xié)議是否支持跨平臺(tái),跨語(yǔ)言。因?yàn)楝F(xiàn)在的架構(gòu)更加靈活,如果存在異構(gòu)系統(tǒng)通信需求,那么這個(gè)是必須要考慮的。
4、可擴(kuò)展性/兼容性,在實(shí)際業(yè)務(wù)開(kāi)發(fā)中,系統(tǒng)往往需要隨著需求的快速迭代來(lái)實(shí)現(xiàn)快速更新,這就要求我們采用的序列化協(xié)議基于良好的可擴(kuò)展性/兼容性,比如在現(xiàn)有的序列化數(shù)據(jù)結(jié)構(gòu)中新增一個(gè)業(yè)務(wù)字段,不會(huì)影響到現(xiàn)有的服務(wù)。
5、技術(shù)的流行程度,越流行的技術(shù)意味著使用的公司多,那么很多坑都已經(jīng)淌過(guò)并且得到了解決,技術(shù)解決方案也相對(duì)成熟。
6、學(xué)習(xí)難度和易用性。

選型建議

1、對(duì)性能要求不高的場(chǎng)景,可以采用基于XML 的SOAP 協(xié)議。
2、對(duì)性能和間接性有比較高要求的場(chǎng)景,那么Hessian、Protobuf、Thrift、Avro 都可以。
3、基于前后端分離,或者獨(dú)立的對(duì)外的api 服務(wù),選用JSON 是比較好的,對(duì)于調(diào)試、可讀性都很不錯(cuò)。
4、Avro 設(shè)計(jì)理念偏于動(dòng)態(tài)類型語(yǔ)言,那么這類的場(chǎng)景使用Avro 是可以的。

學(xué)習(xí)來(lái)源https://www.gupaoedu.com/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容