天天看點

分布式通信協定-序列化(分布式筆記)

Java序列化機制Serializable接口

Java的序列化機制存在的問題

1、序列化資料結果比較大、傳輸效率比較低

2、不能跨語言對接

影響:以至于在後來的很長一段時間,基于XML格式編碼的對象序列化機制成為了主流,一方面解決了多語言相容問題,另一方面比二進制的序列化方式更容易了解。以至于基于XML的SOAP協定及對應的WebService架構在很長一段時間内成為各個主流開發語言的必備的技術。

再到後來,基于JSON的簡單文本格式編碼的HTTP REST接口又基本上取代了複雜的Web Service接口,成為分布式架構中遠端通信的首要選擇。但是JSON序列化存儲占用的空間大、性能低等問題,同時移動用戶端應用需要更高效的傳輸資料來提升使用者體驗。在這種情況下與語言無關并且高效的二進制編碼協定就成為了大家追求的熱點技術之一。首先誕生的一個開源的二進制序列化架構-MessagePack。它比google的Protocol Buffers出現得還要早。

合适的序列化協定不僅可以提高系統的通用性、強壯型、安全性、優化性能。同時還能讓系統更加易于調試和擴充。

REST和RESTful

Representational State Transfer,表述性狀态轉移是一組架構限制條件和原則,需要注意的是,REST是設計風格而不是标準,使用REST風格設計的應用程式就是RESTful。REST通常基于使用HTTP,URI,和XML(标準通用标記語言下的一個子集)以及HTML(标準通用标記語言下的一個應用)這些現有的廣泛流行的協定和标準。

用URL定位資源,用HTTP動詞(GET,POST,DELETE,DETC)描述操作。在設計web接口的時候,REST主要是用于定義接口名,接口名一般是用名詞寫,不用動詞,那怎麼表達“擷取”或者“删除”或者“更新”這樣的操作呢——用請求類型來區分。比如,我們有一個friends接口,對于“朋友”我們有增删改查四種操作,怎麼定義REST風格的接口?

增加一個朋友,uri: generalcode.cn/va/friends 接口類型:POST

删除一個朋友,uri: generalcode.cn/va/friends 接口類型:DELETE

修改一個朋友,uri: generalcode.cn/va/friends 接口類型:PUT

查找朋友, uri: generalcode.cn/va/friends 接口類型:GET

上面我們定義的四個接口就是符合REST協定的,請注意,這幾個接口都沒有動詞,隻有名詞friends,都是通過Http請求的接口類型來判斷是什麼業務操作。舉個反例:generalcode.cn/va/deleteFriends 該接口用來表示删除朋友,這就是不符合REST協定的接口。一般接口的傳回值是JSON或者XML類型的,網際網路項目一般都是JSON類型的。

用HTTP Status Code傳遞Server的狀态資訊。比如最常用的 200 表示成功,500 表示Server内部錯誤,403表示Bad Request等。(反例:傳統web開發傳回的狀态碼一律都是200,其實不可取。)那這種風格的接口有什麼好處呢?前後端分離。前端拿到資料隻負責展示和渲染,不對資料做任何處理。後端處理資料并以JSON格式傳輸出去,定義這樣一套統一的接口,在web,ios,android三端都可以用相同的接口,是不是很爽?!

序列化和反序列化

1. 什麼是序列化?反序列化?

Java中對象的序列化指的是将對象轉換成以位元組序列的形式來表示,這些位元組序列包含了對象的資料和資訊,一個序列化後的對象可以被寫到資料庫或檔案中,也可用于網絡傳輸,一般當我們使用緩存cache(記憶體空間不夠有可能會本地存儲到硬碟)或遠端調用rpc(網絡傳輸)的時候,經常需要讓我們的實體類實作Serializable接口,目的就是為了讓其可序列化,然後使用 ObjectInputStream 和 ObjectOutputStream 進行對象的讀寫。當然,序列化後的最終目的是為了反序列化,恢複成原先的Java對象

seriallization 序列化:将對象轉化為便于傳輸的格式, 常見的序列化格式:二進制格式,位元組數組,json字元串,xml字元串。deseriallization 反序列化:将序列化的資料恢複為對象的過程。

比如:現在我們都會在淘寶上買桌子,桌子這種很不規則不東西,該怎麼從一個城市運輸到另一個城市,這時候一般都會把它拆掉成闆子,再裝到箱子裡面,就可以快遞寄出去了,這個過程就類似我們的序列化的過程(把資料轉化為可以存儲或者傳輸的形式)。當買家收到貨後,就需要自己把這些闆子組裝成桌子的樣子,這個過程就像反序列 的過程(轉化成當初的資料對象)。

2、為什麼要序列化?

我們都知道,在進行浏覽器通路的時候,我們看到的文本、圖檔、音頻、視訊等都是通過二進制序列進行傳輸的,那麼如果我們需要将Java對象進行傳輸的時候,是不是也應該先将對象進行序列化?答案是肯定的,我們需要先将Java對象進行序列化,然後通過網絡,IO進行傳輸,當到達目的地之後,再進行反序列化擷取到我們想要的對象,最後完成通信。

3、怎麼去實作一個序列化操作

1、實作Serializable接口

2、ObjectInputStream:讀取指定的位元組資料轉換成對象

3、ObjectOutputStream:将指定對象轉換成位元組資料

4、serialVersionUID的作用

檔案流中的class和classpath中的class,也就是修改過後的class,不相容了,處于安全機制考慮,程式抛出了錯誤,并且拒絕載入。從錯誤結果來看,如果沒有為指定的class配置serialVersionUID,那麼java編譯器會自動給這個class進行一個摘要算法,類似于指紋算法,隻要這個檔案有任何改動,得到的UID就會截然不同的,而為指定的class配置serialVersionUID可以保證在這麼多類中,這個編号是唯一的。是以,由于沒有事先指定 serialVersionUID,編譯器又為我們生成了一個UID,當然和前面儲存在檔案中的那個不會一樣了,于是就出現了2個序列化版本号不一緻的錯誤。是以,隻要我們自己指定了serialVersionUID,就可以在序列化後,去添加一個字段,或者方法,而不會影響到後期的還原,還原後的對象照樣可以使用,而且還多了方法或者屬性可以用。

5、靜态變量的序列化

序列化并不儲存靜态變量的值,是以,序列化->修改靜态變量和成員變量的值->反序列化->通路反序列 對象的靜态變量和成員變量 靜态變量的值是修改後的 成員變量的值是修改前的

6、Transient關鍵字

transient關鍵字的作用,讓某些被修飾的成員屬性變量不會被序列化。

Transient關鍵字的使用時機:類中的字段值可以根據其它字段推導出來,如一個長方形類有三個屬性:長度、寬度、面積(示例而已,一般不會這樣設計),那麼在序列化的時候,面積這個屬性就沒必要被序列化了。

為什麼使用transient:主要是為了節省存儲空間,比如類的成員變量中有一些備援的大對象,其它的感覺沒啥好處,可能還有壞處(有些字段可能需要重新計算,初始化什麼的),總的來說,利大于弊。

7、父子類問題

如果父類沒有實作序列化,而子類實作列序列化。那麼子類從父類繼承的成員将無法序列化。

8、序列化的存儲規則

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
   Test test = new Test();
   //試圖将對象兩次寫入檔案
   out.writeObject(test);
   out.flush();
   System.out.println(new File("result.obj").length());
   out.writeObject(test);
   out.close();
   System.out.println(new File("result.obj").length());
 
   ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
   //從檔案依次讀出兩個檔案
   Test t1 = (Test) oin.readObject();
   Test t2 = (Test) oin.readObject();
   oin.close();
            
   //判斷兩個引用是否指向同一個對象
   System.out.println(t1 == t2);
           

代碼中對同一對象兩次寫入檔案,列印出寫入一次對象後的存儲大小和寫入兩次後的存儲大小,然後從檔案中反序列化出兩個對象,比較這兩個對象是否為同一對象。一 般的思維是,兩次寫入對象,檔案大小會變為兩倍的大小,反序列化時,由于從檔案讀取,生成了兩個對象,判斷相等時應該是輸入 false 才對,但是最後結果輸出如下圖所示。

分布式通信協定-序列化(分布式筆記)

Java 序列化機制為了節省磁盤空間,具有特定的存儲規則,當寫入檔案的為同一對象時,并不會再将對象的内容進行存儲,而隻是再次存儲一份引用,上面增加的 5 位元組的存儲空間就是新增引用和一些控制資訊的空間。反序列化時,恢複引用關系,使得代碼 中的 t1 和 t2 指向唯一的對象,二者相等,輸出 true。該存儲規則極大的節省了存儲空間。

序列化的實際應用

1、 什麼是深度克隆、淺度克隆?如何用序列化實作深度克隆?

Object中的克隆方法是淺度克隆,JDK規定了克隆需要滿足的一些條件,簡要總結一下就是:對某個對象進行克隆,對象的的成員變量如果包括引用類型或者數組,那麼克隆的時候其實是不會把這些對象也帶着複制到克隆出來的對象裡面的,隻是複制一個引用,這個引用指向被克隆對象的成員對象,但是基本資料類型是會跟着被帶到克隆對象裡面去的。而深度可能就是把對象的所有屬性都統統複制一份新的到目标對象裡面去。簡單畫個圖:

分布式通信協定-序列化(分布式筆記)
public class CloneDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Teacher teacher=new Teacher();
        teacher.setName("mic");
        Student student=new Student();
        student.setName("沐風");
        student.setAge(35);
        student.setTeacher(teacher);
        Student student2=(Student) student.deepClone(); //克隆一個對象
        System.out.println(student);
        student2.getTeacher().setName("james");
        System.out.println(student2);
    }
}

public class Student implements Serializable{
    private static final long serialVersionUID = 5630895052908986144L;
    private String name;
    private int age;
    private Teacher teacher;
    public Object deepClone() throws IOException, ClassNotFoundException {
        //序列化
        ByteArrayOutputStream baos=new ByteArrayOutputStream();
        ObjectOutputStream oos=new ObjectOutputStream(baos);
        oos.writeObject(this);
        //反序列化
        ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois=new ObjectInputStream(bais);
        return ois.readObject();
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", teacher=" + teacher +
                '}';
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Teacher getTeacher() {
        return teacher;
    }
    public void setTeacher(Teacher teacher) {
        this.teacher = teacher;
}
}

public class Teacher implements Serializable{
    private static final long serialVersionUID = -6635991328204468281L;
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Teacher{" +
                "name='" + name + '\'' +
                '}';
    }
}
           

總結

1、在java中,隻要一個類實作了java.io.Serializable接口,那麼它就可以被序列化

2、通過ObjectOutputStream和ObjectInputStream将對象進行序列化和反序列化

3、對象是否允許被反序列化,不僅僅是取決于對象的代碼是否一緻,同時還有一個重要的因素(UID)

4、靜态變量不會被序列化

5、要将從父類繼承的屬性序列化,那麼父類也必須實作Serializable接口

6、transient關鍵字,主要是控制變量是否能夠被序列化。沒有被序列化的(transient)成員變量反序列化後,會被設定成初始值,比如String -> null

7、通過序列化操作可實作深度克隆

主流的序列化技術有哪些

使用JAVA進行序列化有它的優點,也有它的缺點。

優點:JAVA語言本身提供,使用比較友善和簡單。

缺點:不支援跨語言處理,性能相對不是很好,序列化以後産生的資料相對較大。

XML序列化架構

XML序列化的好處在于可讀性好,友善閱讀和調試。但是序列化以後的位元組碼檔案比較大,而且效率不高,适用于對性能不高,而且QPS較低的企業級内部系統之間的資料交換的場景,同時XML又具有語言無關性,是以還可以用于異構系統之間的資料交換和協定。比如我們熟知的Webservice,就是采用XML格式對資料進行序列化的。

JSON序列化架構

JSON(JavaScript Object Notation)是一種輕量級的資料交換格式,相對于XML來說,JSON 的位元組流更小,而且可讀性也非常好。現在JSON資料格式在企業運用是最普遍的,JSON序列化常用的開源工具有很多。

  1. Jackson (https://github.com/FasterXML/jackson)
  2. 阿裡開源的 FastJson (https://github.com/alibaba/fastjon)
  3. Google的GSON (https://github.com/google/gson)

    這幾種json序列化工具中,Jackson與fastjson要比GSON的性能要好,但是Jackson、GSON的穩定性要比Fastjson好,而fastjson的優勢在于提供的api非常容易使用。

Hessian序列化架構

Hessian是一個支援跨語言傳輸的二進制序列化協定,相對于Java預設的序列化機制來說,Hessian具有更好的性能和易用性,而且支援多種不同的語言。實際上Dubbo采用的就是Hessian序列化來實作,隻不過Dubbo對Hessian進行了重構,性能更高。

Protobuf 序列化架構

Protobuf是Google的一種資料交換格式,它獨立于語言、獨立于平台。

Google提供了多種語言來實作,比如Java、C、Go、Python,每一種實作都包含了相應語言的編譯器和庫檔案。Protobuf使用比較廣泛,主要是空間開銷小和性能比較好,非常适合用于公司内部對性能要求高的RPC調用。 另外由于解析性能比較高,序列化以後資料量相對較少,是以也可以應用在對象的持久化場景中。但是要使用Protobuf會相對來說麻煩些,因為他有自己的文法,有自己的編譯器。

百度有提供一個自己封裝的 且便于使用的Protobuf。

<dependency>
    <groupId>com.baidu</groupId>
    <artifactId>jprotobuf</artifactId>
    <version>2.1.2</version>
</dependency>
           

繼續閱讀