Java Serialization(Java 序列化)

序列化是什么



信息的傳遞、交換支撐整個(gè)互聯(lián)網(wǎng)產(chǎn)業(yè),那么信息的交流的過程中遵循著什么樣的標(biāo)準(zhǔn)。常見的網(wǎng)絡(luò)傳輸協(xié)議有 TCP/IP 協(xié)議,OSI協(xié)議等模型。我們的通訊協(xié)議往往根據(jù)不同的應(yīng)用場景采用不同分層模型,因不同模型功能定義不一樣,因此粒度的劃分也有所不同,比如:TCP/IP協(xié)議是一個(gè)四層協(xié)議,而OSI模型卻是七層協(xié)議模型。

在 TCP/IP 協(xié)議模型中分為:網(wǎng)絡(luò)接口層、網(wǎng)際層、傳輸層、應(yīng)用層。OSI 協(xié)議模型分為:物理層、數(shù)據(jù)鏈路層,網(wǎng)絡(luò)層、傳輸層、會(huì)話層、表示層以及我們的應(yīng)用層。

在 OSI 模型中的表示層主要負(fù)責(zé)把應(yīng)用層中應(yīng)用程序的對象轉(zhuǎn)換成對應(yīng)的二進(jìn)制串或者將二進(jìn)串轉(zhuǎn)換成我們應(yīng)用程序中對象。而這兩個(gè)轉(zhuǎn)換的過程便被稱作為序列化和反序列化,因此序列化和反序列屬于通信協(xié)議的一部分。就一般而言,TCP/IP 模型中的應(yīng)用層對應(yīng)著 OSI 模型中的應(yīng)用層、展示層、會(huì)話層, 因此序列化協(xié)議屬于 TCP/IP 協(xié)議應(yīng)用層的一部分。

+ 序列化和反序列化屬于通信協(xié)議的一部分。
+ 序列化:將數(shù)據(jù)結(jié)構(gòu)(C)或?qū)ο?java)轉(zhuǎn)換成二進(jìn)制串的過程。
+ 反序列化:將在序列化過程中所生成的二進(jìn)制串轉(zhuǎn)換成數(shù)據(jù)結(jié)構(gòu)(C)或者對象(java)的過程。

序列化有什么特點(diǎn)



序列化協(xié)議的實(shí)現(xiàn)有很多種,Java Serialization 只是眾多實(shí)現(xiàn)方式中的一種。好的序列化實(shí)現(xiàn)方式需要從以下點(diǎn)去考慮:通用型、強(qiáng)壯性、可擴(kuò)展性/兼容性以及安全性。

  • 通用性
    序列化是否支持跨平臺、夸語言,學(xué)習(xí)成本是否足夠低,以及流行度怎么樣;流行度從側(cè)面也反映該它對跨平臺、跨語言的支持度,以及它社區(qū)活躍度等等。

  • 強(qiáng)健性
    強(qiáng)壯性也被稱為魯棒性。一門好的技術(shù),工具框架往往是經(jīng)過長的時(shí)間去持續(xù)的優(yōu)化、大而全的測試,慢慢被大眾所接受。但是,為了支撐跨平臺、跨語言或者是支持某一中特性去犧牲我們的強(qiáng)健性。

  • 可調(diào)試性/可讀性
    序列化和反序列化作用于對象和二進(jìn)制間相互轉(zhuǎn)化的過程。在轉(zhuǎn)化的過程一旦出錯(cuò),由于二進(jìn)制數(shù)據(jù)的不可讀性,那么就可能需要花費(fèi)大量的時(shí)間去調(diào)試。因此實(shí)現(xiàn)方案有沒有提供相應(yīng)的文檔、調(diào)試工具、平臺作為支撐這也是在技術(shù)選型的過程需要考量的。

  • 性能
    討論性能就是討論時(shí)間復(fù)雜度、空間復(fù)雜度。就 Java 而言,序列化將會(huì)在需要序列化的實(shí)例對象上加上類描述,數(shù)據(jù)上加上字段描述以用于反序列化。這樣不僅增加了在網(wǎng)絡(luò)傳輸?shù)倪^程中帶寬的開銷,也增加了在持久化的過程中磁盤的開銷。再有復(fù)雜的實(shí)現(xiàn)方式在序列化和反序列化都會(huì)增加相應(yīng)的時(shí)間開銷。

  • 可擴(kuò)展性/兼容性
    在當(dāng)下的互聯(lián)網(wǎng)時(shí)代,出陳推新,軟件迭代非??欤瑧?yīng)用程序中的業(yè)務(wù)實(shí)體對象字段的增減是否支持就服務(wù),對以前經(jīng)過序列化而持久化的數(shù)據(jù)在反序列化的過程是否兼容?

  • 安全性/訪問限制
    序列化會(huì)把業(yè)務(wù)數(shù)據(jù)完全存儲(chǔ)到二進(jìn)制流中,而且這個(gè)過程是可逆的、可見的,因此給安全帶來了極大的挑戰(zhàn)。

Java 序列化的代價(jià)

降低了“改變這個(gè)類的實(shí)現(xiàn)”的靈活性

如果你的類實(shí)現(xiàn)了序列化,而且這個(gè)類得到了廣泛的應(yīng)用,就得持久支持此類的序列化形式。而倘若你的實(shí)現(xiàn)方式僅僅采用的是 Java 默認(rèn)的實(shí)現(xiàn)方式,那么這種序列化形式將會(huì)永遠(yuǎn)束縛這個(gè)類最初的內(nèi)部表示方法(功能代碼等)。接受了這種默認(rèn)的序列化形式,以后需要改變這個(gè)類的內(nèi)部結(jié)構(gòu)就有可能導(dǎo)致前后序列化形式的不兼容,使用新的版本進(jìn)行反序列有可能導(dǎo)致失敗。

每個(gè)可序列化的類都有一個(gè)都有一個(gè)唯一的標(biāo)識號與其關(guān)聯(lián),也就是我們通常說的 serialVersionUID。如果可序列化的類沒有顯示的聲明該常量,Java 虛擬機(jī)會(huì)自動(dòng)根據(jù)這個(gè)類調(diào)用一個(gè)復(fù)雜的運(yùn)算過程,從而在產(chǎn)生運(yùn)行時(shí)的 serialVersionUID。這個(gè)自動(dòng)生成的值將會(huì)受到類名、實(shí)現(xiàn)的接口名稱、以及所有的公有方法和受保護(hù)的成員名稱所影響。如果你通過任何方式改變了這些信息,兼容性都會(huì)遭到破壞,在運(yùn)行時(shí)會(huì)導(dǎo)致 InvalidClassException 異常。

如果你沒有顯示的聲明 serialVersionUID,就算類的信息沒有發(fā)生改變,在跨 Java 虛擬機(jī)進(jìn)行反序列化的時(shí)候也有可能失敗。不同 Java 虛擬機(jī)序列化的實(shí)現(xiàn)方式上可能有所不同,這些細(xì)微的差別都有可能導(dǎo)致運(yùn)行時(shí)生成的 serialVersionUID 不一致從而導(dǎo)致反序列化失敗。

它增加了出現(xiàn)BUG和安全漏洞的可能性

序列化給我們的應(yīng)用程序會(huì)帶來一系列的安全問題。無論是接收了默認(rèn)的序列化行為,還是實(shí)現(xiàn)自己的了序列化方法,反序列化就像提供了一個(gè)“隱藏的構(gòu)造器”,它具備了和其他構(gòu)造器相同的特點(diǎn),這樣就為攻擊者留下了攻擊的漏洞。最典型的就是 Struts 2.x 經(jīng)常爆出反序列化的漏洞。

隨著版本的迭代更新,相關(guān)的測試負(fù)擔(dān)也增加

當(dāng)一個(gè)可序列類被修訂后,很重要的一點(diǎn)是,要檢查是否可以“在新版本中序列化一個(gè)實(shí)例,然后在舊版本中是否可以被反序列化。舊版本中序列化的實(shí)例在新版本中是否仍可以做反序列化操作”。因此,測試所需要的工作量與“可序列化類的數(shù)量和發(fā)行的版本號”的乘積成正比。因此在最初編寫一個(gè)可序列化類的時(shí)候就應(yīng)該精心設(shè)計(jì)自定義的序列化形式,這樣測試的要求便會(huì)降低。

Java 序列化應(yīng)該知道幾件事

序列化ID問題 (serialVersionUID)

雖然兩個(gè)類的功能代碼完全一致,但是序列化 ID 不同,他們無法相互序列化和反序列化。Eclipse 提供了兩種生成策略:一個(gè)是固定的 1L;一個(gè)是隨機(jī)生成一個(gè)不重復(fù)的 long 類型數(shù)據(jù)(實(shí)際上是使用 JDK 工具 serialver 生成)。在沒有特殊需求,采用默認(rèn)的就好,這樣可以確保代碼一致時(shí)反序列化成功。

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

Java 序列化是不保存靜態(tài)變量。Java 序列化的本質(zhì)是為了存儲(chǔ)、傳輸實(shí)例對象數(shù)據(jù),序列化的是實(shí)例對象的成員變量,而靜態(tài)變量、常量都屬于類的成員。

父類的序列化與 Transient 關(guān)鍵字

一個(gè)子類實(shí)現(xiàn)了 Serializable 接口,它的父類都沒有實(shí)現(xiàn) Serializable 接口,序列化該子類對象,然后反序列化后輸出父類定義的某變量的數(shù)值,該變量數(shù)值與序列化時(shí)的數(shù)值不同。

要想將父類對象也序列化,就需要讓父類也實(shí)現(xiàn)Serializable 接口。如果父類不實(shí)現(xiàn)的話的,就 需要有默認(rèn)的無參的構(gòu)造函數(shù)。在父類沒有實(shí)現(xiàn) Serializable 接口時(shí),虛擬機(jī)是不會(huì)序列化父對象的,而一個(gè) Java 對象的構(gòu)造必須先有父對象,才有子對象,反序列化也不例外。所以反序列化時(shí),為了構(gòu)造父對象,只能調(diào)用父類的無參構(gòu)造函數(shù)作為默認(rèn)的父對象。因此當(dāng)我們?nèi)「笇ο蟮淖兞恐禃r(shí),它的值是調(diào)用父類無參構(gòu)造函數(shù)后的值。如果你考慮到這種序列化的情況,在父類無參構(gòu)造函數(shù)中對變量進(jìn)行初始化,否則的話,父類變量值都是默認(rèn)聲明的值,如 int 型的默認(rèn)是 0,string 型的默認(rèn)是 null。

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

對敏感字段加密(模糊化數(shù)據(jù))

我們的實(shí)體對象可能存在敏感數(shù)據(jù),比如年齡、身份信息,銀行卡號等等。由于在某一些業(yè)務(wù)場景會(huì)在網(wǎng)絡(luò)中進(jìn)行傳輸,也有可能持久化到日志文件、硬盤、數(shù)據(jù)庫等,這時(shí)候便需要讀該部分?jǐn)?shù)據(jù)進(jìn)行模糊化處理,甚至是加密簽名等處理。這樣這些信息只能通過反序列化才能進(jìn)行有效的讀取,一定程度保證序列化對象的數(shù)據(jù)安全。

使用序列化代理代替序列化實(shí)例

前面談到實(shí)現(xiàn) Serializable 接口,會(huì)增加出錯(cuò)和出現(xiàn)安全問題的可能性,因?yàn)樗翘鍪褂闷胀?gòu)造方法之外的機(jī)制來創(chuàng)建的實(shí)例對象,然可使用序列化代理模式會(huì)大大的降低此類的風(fēng)險(xiǎn)。

Java 序列化的簡單應(yīng)用(Java clone)



Java 序列化較為常見的就是用于深克隆,前提是需要克隆的對象實(shí)現(xiàn)了序列化。

private static Serializable clone(Serializable object) throws IOException, ClassNotFoundException{
        //將對象寫到流里  
         ByteArrayOutputStream bo =new ByteArrayOutputStream();  
         ObjectOutputStream oo=new ObjectOutputStream(bo);  
         oo.writeObject(object);  
         
         //從流里讀出來  
         ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());  
         ObjectInputStream oi=new ObjectInputStream(bi);
         
        return (Serializable) oi.readObject();  
    }

Java 序列化基本實(shí)現(xiàn)

在Java中,只要一個(gè)類實(shí)現(xiàn)了java.io.Serializable接口,那么它就可以被序列化。

Person.java

package me.knight.serialize.model;

import java.io.Serializable;

public class Person implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private String name;
    private Address addr;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Address getAddr() {
        return addr;
    }
    
    public void setAddr(Address addr) {
        this.addr = addr;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", addr=" + addr + "]";
    }
}

Address.java

package me.knight.serialize.model;

import java.io.Serializable;


public class Address implements  Serializable{
    
    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    
    private String street;

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    @Override
    public String toString() {
        return "Address [street=" + street + "]";
    }
    
}

SerializeUtil.java

package me.knight.serialize;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import me.knight.serialize.model.Address;
import me.knight.serialize.model.Person;

public class SerializeUtil {
    public static void main(String[] args) throws IOException,
            ClassNotFoundException {
        Person origin = getPerson();
        serialize(getPerson());
        
        Person clone = deserializePerson();
        
        System.out.println("p1 和 p2 是否是同一對象:" + (origin == clone));
        System.out.println("p1:" + origin);
        System.out.println("p2:" + clone);
        
        System.out.println("p1 和 p2 是否是同一對象:" + (origin.getAddr() == clone.getAddr()));
        System.out.println("p1 和 p2 的地址是否相同:" + origin.getAddr().toString().equals(clone.getAddr().toString()));
    }

    public static Person serialize(Person person) throws IOException {
        // ObjectOutputStream
        // 對象輸出流,將Person對象存儲(chǔ)到D盤的serialize_person文件中,完成對Person對象的序列化操作
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(
                new File("D:/serialize_person.txt")));
        oo.writeObject(person);
        System.out.println("Person對象序列化成功!");
        oo.close();

        return person;
    }

    private static Person deserializePerson() throws IOException,ClassNotFoundException {
        @SuppressWarnings("resource")
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:/serialize_person.txt")));
    
        Person person = (Person) ois.readObject();
        System.out.println("Person對象反序列化成功!");
        return person;
    }

    private static Person getPerson() {
        Person person = new Person();
        person.setName("非典型程序員");

        Address addr = new Address();
        addr.setStreet("程序員胡同168號");

        person.setAddr(addr);

        return person;
    }
}

輸出結(jié)果

Person對象序列化成功!
Person對象反序列化成功!
p1 和 p2 是否是同一對象:false
p1:Person [name=非典型程序員, addr=Address [street=程序員胡同168號]]
p2:Person [name=非典型程序員, addr=Address [street=程序員胡同168號]]
p1 和 p2 是否是同一對象:false
p1 和 p2 的地址是否相同:true
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • JAVA序列化機(jī)制的深入研究 對象序列化的最主要的用處就是在傳遞,和保存對象(object)的時(shí)候,保證對象的完整...
    時(shí)待吾閱讀 11,188評論 0 24
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,797評論 11 349
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,687評論 18 399
  • (一)看煙火 小鎮(zhèn)每年正月十五,都會(huì)在鎮(zhèn)政府門口的廣場上放煙火。那是一年最盛大的日子了,父母帶著我們早早去廣場占據(jù)...
    chenum1閱讀 505評論 0 0
  • 三月的一個(gè)早晨,一串清脆婉轉(zhuǎn)的黃鸝歌聲把我從久久的冬眠中喚醒。我睜開眼睛好奇的欣賞著這世界。三月的早晨,天...
    BlueCute閱讀 312評論 0 2

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