你不知道的java對象序列化的祕密

簡介

你知道序列化可使用代理嗎?你知道序列化的安全性嗎?每一個java程序員都據說過序列化,要存儲對象須要序列化,要在網絡上傳輸對象要序列化,看起來很簡單的序列化其實裏面還隱藏着不少小祕密,今天本文將會爲你們一一揭祕。java

更多精彩內容且看:程序員

更多內容請訪問 www.flydean.com

什麼是序列化

序列化就是將java對象按照必定的順序組織起來,用於在網絡上傳輸或者寫入存儲中。而反序列化就是從網絡中或者存儲中讀取存儲的對象,將其轉換成爲真正的java對象。算法

因此序列化的目的就是爲了傳輸對象,對於一些複雜的對象,咱們可使用第三方的優秀框架,好比Thrift,Protocol Buffer等,使用起來很是的方便。安全

JDK自己也提供了序列化的功能。要讓一個對象可序列化,則能夠實現java.io.Serializable接口。網絡

java.io.Serializable是從JDK1.1開始就有的接口,它其實是一個marker interface,由於java.io.Serializable並無須要實現的接口。繼承java.io.Serializable就代表這個class對象是能夠被序列化的。框架

@Data
@AllArgsConstructor
public class CustUser implements java.io.Serializable{
    private static final long serialVersionUID = -178469307574906636L;
    private String name;
    private String address;
}

上面咱們定義了一個CustUser可序列化對象。這個對象有兩個屬性:name和address。函數

接下看下怎麼序列化和反序列化:性能

public void testCusUser() throws IOException, ClassNotFoundException {
        CustUser custUserA=new CustUser("jack","www.flydean.com");
        CustUser custUserB=new CustUser("mark","www.flydean.com");

        try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(custUserA);
            objectOutputStream.writeObject(custUserB);
        }
        
        try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            CustUser custUser1 = (CustUser) objectInputStream.readObject();
            CustUser custUser2 = (CustUser) objectInputStream.readObject();
            log.info("{}",custUser1);
            log.info("{}",custUser2);
        }
    }

上面的例子中,咱們實例化了兩個CustUser對象,並使用objectOutputStream將對象寫入文件中,最後使用ObjectInputStream從文件中讀取對象。區塊鏈

上面是最基本的使用。須要注意的是CustUser class中有一個serialVersionUID字段。this

serialVersionUID是序列化對象的惟一標記,若是class中定義的serialVersionUID和序列化存儲中的serialVersionUID一致,則代表這兩個對象是一個對象,咱們能夠將存儲的對象反序列化。

若是咱們沒有顯示的定義serialVersionUID,則JVM會自動根據class中的字段,方法等信息生成。不少時候我在看代碼的時候,發現不少人都將serialVersionUID設置爲1L,這樣作是不對的,由於他們沒有理解serialVersionUID的真正含義。

重構序列化對象

假如咱們有一個序列化的對象正在使用了,可是忽然咱們發現這個對象好像少了一個字段,要把他加上去,可不能夠加呢?加上去以後原序列化過的對象能不能轉換成這個新的對象呢?

答案是確定的,前提是兩個版本的serialVersionUID必須同樣。新加的字段在反序列化以後是空值。

序列化不是加密

有不少同窗在使用序列化的過程當中可能會這樣想,序列化已經將對象變成了二進制文件,是否是說該對象已經被加密了呢?

這實際上是序列化的一個誤區,序列化並非加密,由於即便你序列化了,仍是能從序列化以後的數據中知道你的類的結構。好比在RMI遠程調用的環境中,即便是class中的private字段也是能夠從stream流中解析出來的。

若是咱們想在序列化的時候對某些字段進行加密操做該怎麼辦呢?

這時候能夠考慮在序列化對象中添加writeObject和readObject方法:

private String name;
    private String address;
    private int age;

    private void writeObject(ObjectOutputStream stream)
            throws IOException
    {
        //給age加密
        age = age + 2;
        log.info("age is {}", age);
        stream.defaultWriteObject();
    }

    private void readObject(ObjectInputStream stream)
            throws IOException, ClassNotFoundException
    {
        stream.defaultReadObject();
        log.info("age is {}", age);
        //給age解密
        age = age - 2;
    }

上面的例子中,咱們爲CustUser添加了一個age對象,並在writeObject中對age進行了加密(加2),在readObject中對age進行了解密(減2)。

注意,writeObject和readObject都是private void的方法。他們的調用是經過反射來實現的。

使用真正的加密

上面的例子, 咱們只是對age字段進行了加密,若是咱們想對整個對象進行加密有沒有什麼好的處理辦法呢?

JDK爲咱們提供了javax.crypto.SealedObject 和java.security.SignedObject來做爲對序列化對象的封裝。從而將整個序列化對象進行了加密。

仍是舉個例子:

public void testCusUserSealed() throws IOException, ClassNotFoundException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException {
        CustUser custUserA=new CustUser("jack","www.flydean.com");
        Cipher enCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        Cipher deCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKey secretKey = new SecretKeySpec("saltkey111111111".getBytes(), "AES");
        IvParameterSpec iv = new IvParameterSpec("vectorKey1111111".getBytes());
        enCipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
        deCipher.init(Cipher.DECRYPT_MODE,secretKey,iv);
        SealedObject sealedObject= new SealedObject(custUserA, enCipher);

        try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(sealedObject);
        }

        try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            SealedObject custUser1 = (SealedObject) objectInputStream.readObject();
            CustUser custUserV2= (CustUser) custUser1.getObject(deCipher);
            log.info("{}",custUserV2);
        }
    }

上面的例子中,咱們構建了一個SealedObject對象和相應的加密解密算法。

SealedObject就像是一個代理,咱們寫入和讀取的都是這個代理的加密對象。從而保證了在數據傳輸過程當中的安全性。

使用代理

上面的SealedObject實際上就是一種代理,考慮這樣一種狀況,若是class中的字段比較多,而這些字段均可以從其中的某一個字段中自動生成,那麼咱們其實並不須要序列化全部的字段,咱們只把那一個字段序列化就能夠了,其餘的字段能夠從該字段衍生獲得。

在這個案例中,咱們就須要用到序列化對象的代理功能。

首先,序列化對象須要實現writeReplace方法,表示替換成真正想要寫入的對象:

public class CustUserV3 implements java.io.Serializable{

    private String name;
    private String address;

    private Object writeReplace()
            throws java.io.ObjectStreamException
    {
        log.info("writeReplace {}",this);
        return new CustUserV3Proxy(this);
    }
}

而後在Proxy對象中,須要實現readResolve方法,用於從系列化過的數據中重構序列化對象。以下所示:

public class CustUserV3Proxy implements java.io.Serializable{

    private String data;

    public CustUserV3Proxy(CustUserV3 custUserV3){
        data =custUserV3.getName()+ "," + custUserV3.getAddress();
    }

    private Object readResolve()
            throws java.io.ObjectStreamException
    {
        String[] pieces = data.split(",");
        CustUserV3 result = new CustUserV3(pieces[0], pieces[1]);
        log.info("readResolve {}",result);
        return result;
    }
}

咱們看下怎麼使用:

public void testCusUserV3() throws IOException, ClassNotFoundException {
        CustUserV3 custUserA=new CustUserV3("jack","www.flydean.com");

        try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(custUserA);
        }

        try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            CustUserV3 custUser1 = (CustUserV3) objectInputStream.readObject();
            log.info("{}",custUser1);
        }
    }

注意,咱們寫入和讀出的都是CustUserV3對象。

Serializable和Externalizable的區別

最後咱們講下Externalizable和Serializable的區別。Externalizable繼承自Serializable,它須要實現兩個方法:

void writeExternal(ObjectOutput out) throws IOException;
 void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

何時須要用到writeExternal和readExternal呢?

使用Serializable,Java會自動爲類的對象和字段進行對象序列化,可能會佔用更多空間。而Externalizable則徹底須要咱們本身來控制如何寫/讀,比較麻煩,可是若是考慮性能的話,則可使用Externalizable。

另外Serializable進行反序列化不須要執行構造函數。而Externalizable須要執行構造函數構造出對象,而後調用readExternal方法來填充對象。因此Externalizable的對象須要一個無參的構造函數。

總結

本文詳細分析了序列化對象在多種狀況下的使用,並講解了Serializable和Externalizable的區別,但願你們可以喜歡。

本文做者:flydean程序那些事

本文連接:http://www.flydean.com/java-serialization/

本文來源:flydean的博客

歡迎關注個人公衆號:程序那些事,更多精彩等着您!

相關文章
相關標籤/搜索