Serialization in Java

  • Serializable in Java
  • Class Refactoring with Serialization and serialVersionUID
  • Java Externalizable Interface
  • Java Serialization Methods
  • Serialization with Inheritance
  • Serialization Proxy Pattern

我們知道Java對象的生存周期跟GC有關(guān),更寬泛一點(diǎn)講,JVM關(guān)閉了,對象自然也就被銷毀了。但是有的時候,我們需要將某些對象保存起來,或者進(jìn)行傳輸,以便以后JVM啟動的時候,又可以重新獲取到對象。這個技術(shù)就是對象持久化技術(shù)。
Java中的Serialization可以將一個對象轉(zhuǎn)成字節(jié)流,我們可以將這個字節(jié)流通過網(wǎng)絡(luò)傳輸?shù)狡渌胤剑蛘弑4娴轿募校蛘叽娴綌?shù)據(jù)庫中。這樣就相當(dāng)于將對象保存下來了。
Java中的Deserialization 就是序列化的反過程,從將字節(jié)流中的內(nèi)容轉(zhuǎn)化成java對象。

Serializable in Java

如果你想要將一個對象序列化,你所需要的做的就是實(shí)現(xiàn)java.io.Serializable接口。這個接口是一個marker interface,沒有成員變量,沒有方法。

Java中對象的序列化是通過ObjectInputStream and ObjectOutputStream兩個流實(shí)現(xiàn)的。我們將一個對象寫入字節(jié)流,就需要ObjectOutputStream,從一個字節(jié)流讀取出對象就需要使用ObjectInputStream。

下面我們看一個簡單的序列化的例子:



import java.io.Serializable;

public class Employee implements Serializable {


    private String name;
    private int id;
    transient private int salary;
    
    @Override
    public String toString(){
        return "Employee{name="+name+",id="+id+",salary="+salary+"}";
    }
    
    //getter and setter methods
    public String getName() {
        return name;
    }

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

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getSalary() {
        return salary;
    }

    public void setSalary(int salary) {
        this.salary = salary;
    }
    
}

可以看到這就是一個簡單的實(shí)現(xiàn)了java.io.Serializable的接口的類,定義了幾個屬性和getter和setter。
我們看到對于salary變量我們使用了transient修飾符,這個修飾符適用于:如果對于對象中的某些成員變量,我們不想將它序列化,就可以用transient修飾符修飾它,這樣它就不會被序列化。

下面,我們將一個序列化的對象保存到文件中,然后從文件反序列讀取這個對象,我們需要使用到j(luò)ava.io.Serializable這兩個類



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

/**
 * A simple class with generic serialize and deserialize method implementations
 * 
 * @author 劉德華
 * 
 */
public class SerializationUtil {

    // deserialize to Object from given file
    public static Object deserialize(String fileName) throws IOException,
            ClassNotFoundException {
        FileInputStream fis = new FileInputStream(fileName);
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object obj = ois.readObject();
        ois.close();
        return obj;
    }

    // serialize the given object and save it to file
    public static void serialize(Object obj, String fileName)
            throws IOException {
        FileOutputStream fos = new FileOutputStream(fileName);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(obj);

        fos.close();
    }

}

我們來測試一下序列化的結(jié)果



import java.io.IOException;

public class SerializationTest {

    
    public static void main(String[] args) {
        String fileName="employee.ser";
        Employee emp = new Employee();
        emp.setId(100);
        emp.setName("Pankaj");
        emp.setSalary(5000);
        
        //serialize to file
        try {
            SerializationUtil.serialize(emp, fileName);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        
        Employee empNew = null;
        try {
            empNew = (Employee) SerializationUtil.deserialize(fileName);
        } catch (ClassNotFoundException | IOException e) {
            e.printStackTrace();
        }
        
        System.out.println("emp Object::"+emp);
        System.out.println("empNew Object::"+empNew);
    }


}

運(yùn)行結(jié)果:

emp Object::Employee{name=Pankaj,id=100,salary=5000}
empNew Object::Employee{name=Pankaj,id=100,salary=0}

因?yàn)閟alary被聲明為transient變量,所以這個成員變量沒有被序列化寫入到文件中,所以反序列的時候,這個值為初始化的默認(rèn)0.
類似的是,對于靜態(tài)變量,static變量,也不會被序列化,因?yàn)閟tatic變量是屬于類的信息,并不屬于對象的信息。

Class Refactoring with Serialization and serialVersionUID

java的序列化對于某些對象的變化會忽略掉。
對于下面這些類的變化,不會影響反序列的過程:

  • 向類中添加一個新的變量
  • 將一個變量從transient轉(zhuǎn)為no-transient
  • 將static變量變成 no-static

想要讓這些類的變化反映到反序列化的過程中,我們需要在類中定義一個serialVersionUID。我們來寫一個測試的方法測試反序列已經(jīng)序列化的類。



import java.io.IOException;

public class DeserializationTest {

    public static void main(String[] args) {

        String fileName="employee.ser";
        Employee empNew = null;
        
        try {
            empNew = (Employee) SerializationUtil.deserialize(fileName);
        } catch (ClassNotFoundException | IOException e) {
            e.printStackTrace();
        }
        
        System.out.println("empNew Object::"+empNew);
        
    }

}

我們在運(yùn)行之前,改變一下類,首先先給類添加一個password屬性,并設(shè)置getter和setter



import java.io.Serializable;

public class Employee implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 7239983150140796558L;
    //private static final long serialVersionUID = -6470090944414208496L;
    private String name;
    private int id;
    transient private int salary;
    private String password;
    
    @Override
    public String toString(){
        return "Employee{name="+name+",id="+id+",salary="+salary+"}";
    }
    
    //getter and setter methods
    public String getName() {
        return name;
    }

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

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getSalary() {
        return salary;
    }

    public void setSalary(int salary) {
        this.salary = salary;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
    
}

運(yùn)行程序,我們發(fā)現(xiàn)對反序列沒有影響,和沒加之前一樣

java.io.InvalidClassException: Employee; local class incompatible: stream classdesc serialVersionUID = -5493348434707939249, local class serialVersionUID = 7239983150140796558
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1829)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1986)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
    at SerializationUtil.deserialize(SerializationUtil.java:22)
    at DeserializationTest.main(DeserializationTest.java:13)
Exception in thread "main" java.lang.NullPointerException
    at DeserializationTest.main(DeserializationTest.java:19)

出現(xiàn)錯誤的原因就是因?yàn)椋靶蛄谢念恠erialVersionUID 和現(xiàn)在這個新的類的serialVersionUID 是不同的,實(shí)際上如果一個類沒有顯示定義serialVersionUID ,它就會自動根據(jù)這個類的成員變量方法,類名等信息,自動計(jì)算一個值分配給這個class一個serialVersionUID。如果你使用IDE的話,你會得到一個警告“The serializable class Employee does not declare a static final serialVersionUID field of type long”

如果我們想要在類改變之后,反序列化的時候,能感知到類的改變,我們就需要顯示的制定一個serialVersionUID。

例如,在這個例子中,我們生成一個serialVersionUID,然后將其序列化,運(yùn)行SerializationTest 程序,然后添加password屬性,在運(yùn)行反序列化的程序,我們發(fā)現(xiàn)這次不會報(bào)錯,而且新添加的屬性也會序列化了

Java Externalizable Interface

我們發(fā)現(xiàn)序列化的過程是程序自動進(jìn)行的。有些時候,我們想在序列化的過程中進(jìn)行一些操作,比如篩選和轉(zhuǎn)換的工作。我們就可以實(shí)現(xiàn) java.io.Externalizable接口,這個接口提供了兩個方法,分別可以讓我們序列化的時候,和反序列化的時候的操作。
writeExternal() and readExternal()

package Externalizable;



import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Person implements Externalizable{

    private int id;
    private String name;
    private String gender;
    
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(id);
        out.writeObject(name+"xyz");
        out.writeObject("abc"+gender);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        id=in.readInt();
        //read in the same order as written
        name=(String) in.readObject();
        if(!name.endsWith("xyz")) throw new IOException("corrupted data");
        name=name.substring(0, name.length()-3);
        gender=(String) in.readObject();
        if(!gender.startsWith("abc")) throw new IOException("corrupted data");
        gender=gender.substring(3);
    }

    @Override
    public String toString(){
        return "Person{id="+id+",name="+name+",gender="+gender+"}";
    }
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

}

可以看到我們將對象轉(zhuǎn)換到流之前,先將成員變量的值改變了,當(dāng)讀取的時候,再將對象的值變回來。通過這種方式,我們可以維持?jǐn)?shù)據(jù)的完整性。如果跟我們預(yù)期的值不一致的時候,我們就可以在讀取的時候拋出異常,說明這個序列化對象可能受到了破壞,下面我們寫一個測試程序來驗(yàn)證

package Externalizable;



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

public class ExternalizationTest {

    public static void main(String[] args) {
        
        String fileName = "person.ser";
        Person person = new Person();
        person.setId(1);
        person.setName("Pankaj");
        person.setGender("Male");
        
        try {
            FileOutputStream fos = new FileOutputStream(fileName);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(person);
            oos.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        FileInputStream fis;
        try {
            fis = new FileInputStream(fileName);
            ObjectInputStream ois = new ObjectInputStream(fis);
            Person p = (Person)ois.readObject();
            ois.close();
            System.out.println("Person Object Read="+p);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        
    }

}

運(yùn)行結(jié)果:

Person Object Read=Person{id=1,name=Pankaj,gender=Male}

目前,我們掌握了兩種序列化對象的方法,分別是java.io.Serializable和java.io.Externalizable,似乎java.io.Externalizable更靈活,可以在寫入流之前和讀成對象之前分別進(jìn)行操作。但實(shí)際上,我們更推薦使用java.io.Serializable。
先賣個小關(guān)子,等你讀完了這篇文章,就知道為什么了

Java Serialization Methods

我們已經(jīng)知道java的序列化過程是自動的,只要我實(shí)現(xiàn)了Serializable接口,實(shí)現(xiàn)這個過程的類有 ObjectInputStream and ObjectOutputStream。但是如果我們有一些數(shù)據(jù)比較敏感,比如密碼,賬戶余額,我們不想暴露這些信息,想要save或者retrieving之前先進(jìn)行一些編碼轉(zhuǎn)碼工作,那么我們就需要用到序列化的四個方法了,這些方法,可以幫我們改變序列化的行為。

  • readObject(ObjectInputStream ois): If this method is present in the class, ObjectInputStream readObject() method will use this method for reading the object from stream.
  • writeObject(ObjectOutputStream oos): If this method is present in the class, ObjectOutputStream writeObject() method will use this method for writing the object to stream. One of the common usage is to obscure the object variables to maintain data integrity.
  • Object writeReplace(): If this method is present, then after serialization process this method is called and the object returned is serialized to the stream.
  • Object readResolve(): If this method is present, then after deserialization process, this method is called to return the final object to the caller program. One of the usage of this method is to implement Singleton pattern with Serialized classes.

通常為了安全性,這些方法的實(shí)現(xiàn)都是聲明為private,子類無法去重寫這些函數(shù)。這樣做僅僅是為了保證序列化過程的安全性。

Serialization with Inheritance

有些時候,我們可能需要繼承一個沒有實(shí)現(xiàn)Serializable接口的類,如果我們依賴自動的序列化行為,超類有一些成員變量就不會被轉(zhuǎn)換到流中,當(dāng)我們獲取對象的時候,也就無法獲取到。

這種情況下,我們就可以用到上面提到的readObject() and writeObject()來解決問題了。通過實(shí)現(xiàn)這兩個方法,我們可以將超類的狀態(tài)也序列化,將來獲取的時候,就可以獲取到了。

package Inheritance;



public class SuperClass {

    private int id;
    private String value;
    
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
    
    
}

SuperClass是一個簡單的java bean,但沒有實(shí)現(xiàn)Serializable接口

package Inheritance;



import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectInputValidation;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class SubClass extends SuperClass implements Serializable, ObjectInputValidation{

    private static final long serialVersionUID = -1322322139926390329L;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    @Override
    public String toString(){
        return "SubClass{id="+getId()+",value="+getValue()+",name="+getName()+"}";
    }
    
    //adding helper method for serialization to save/initialize super class state
    private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
        ois.defaultReadObject();
        
        //notice the order of read and write should be same
        setId(ois.readInt());
        setValue((String) ois.readObject());
        
    }
    
    private void writeObject(ObjectOutputStream oos) throws IOException{
        oos.defaultWriteObject();
        
        oos.writeInt(getId());
        oos.writeObject(getValue());
    }

    @Override
    public void validateObject() throws InvalidObjectException {
        //validate the object here
        if(name == null || "".equals(name)) throw new InvalidObjectException("name can't be null or empty");
        if(getId() <=0) throw new InvalidObjectException("ID can't be negative or zero");
    }
    
}

我們實(shí)現(xiàn)了一個ObjectInputValidation接口,并且實(shí)現(xiàn)了 validateObject() 方法,我們可以對數(shù)據(jù)進(jìn)行一些轉(zhuǎn)換,以保證數(shù)據(jù)的完整性不發(fā)生變化

我們寫一個測試類,來看看我們是否能從流中獲取超類的狀態(tài)信息

package Inheritance;



import java.io.IOException;


public class InheritanceSerializationTest {

    public static void main(String[] args) {
        String fileName = "subclass.ser";
        
        SubClass subClass = new SubClass();
        subClass.setId(10);
        subClass.setValue("Data");
        subClass.setName("Pankaj");
        
        try {
            SerializationUtil.serialize(subClass, fileName);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        
        try {
            SubClass subNew = (SubClass) SerializationUtil.deserialize(fileName);
            System.out.println("SubClass read = "+subNew);
        } catch (ClassNotFoundException | IOException e) {
            e.printStackTrace();
        }
    }

}

運(yùn)行結(jié)果:

SubClass read = SubClass{id=10,value=Data,name=Pankaj}

我們發(fā)現(xiàn)通過這種方式,我們可以實(shí)現(xiàn)序列化超類的信息,即使超類沒有實(shí)現(xiàn)序列化的接口。這個技術(shù)在我們需要序列化一些第三方庫的時候,很有用。

Serialization Proxy Pattern

序列化技術(shù)存在下面這些問題:

  • 類的結(jié)構(gòu)不能改變太多,除非我們重新進(jìn)行序列化。所以,有時候即使我們已經(jīng)不需要最初定義的那些變量了,我們?nèi)匀恍枰A羲麄儭?/p>

  • 同時,序列化也會帶來很多的安全性的問題。一個黑客可能可以通過改變輸出的流序列從而來攻擊系統(tǒng)。例如,一個用戶角色被序列化之后,黑客將它的流改變成admin角色,這樣就會造成很大的危險。

為了解決上面的安全性問題,java序列化使用代理模式來增強(qiáng)安全性。在這個模式中,一個內(nèi)部私有類被用作代理類。這個類持有主類的狀態(tài)。這個模式主要通過readResolve() and writeReplace() 這兩個方法實(shí)現(xiàn)的

我們先看代碼再來分析

import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Data implements Serializable{

    private static final long serialVersionUID = 2087368867376448459L;

    private String data;
    
    public Data(String d){
        this.data=d;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
    
    @Override
    public String toString(){
        return "Data{data="+data+"}";
    }
    
    //serialization proxy class
    private static class DataProxy implements Serializable{
    
        private static final long serialVersionUID = 8333905273185436744L;
        
        private String dataProxy;
        private static final String PREFIX = "ABC";
        private static final String SUFFIX = "DEFG";
        
        public DataProxy(Data d){
            //obscuring data for security
            this.dataProxy = PREFIX + d.data + SUFFIX;
        }
        
        private Object readResolve() throws InvalidObjectException {
            if(dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)){
            return new Data(dataProxy.substring(3, dataProxy.length() -4));
            }else throw new InvalidObjectException("data corrupted");
        }
        
    }
    
    //replacing serialized object to DataProxy object
    private Object writeReplace(){
        return new DataProxy(this);
    }
    
    private void readObject(ObjectInputStream ois) throws InvalidObjectException{
        throw new InvalidObjectException("Proxy is not used, something fishy");
    }
}
  • data 和dataProxy類都必須實(shí)現(xiàn)Serializable接口

  • DataProxy類需要持有序列化對象的狀態(tài)

  • DataProxy是一個內(nèi)部的私有類,所以其他類都不可以訪問這個類

  • DataProxy只有一個構(gòu)造函數(shù),就是將data類作為參數(shù),也就是被代理對象需要傳給dataProxy

  • data類需要覆蓋writeReplace() 這個方法會在寫入流之后調(diào)用,這個方法返回dataproxy對象,所以當(dāng)data類被序列化之后,返回的流是dataproxy類。由于dataproxy類在外部是無法訪問的,所以它不可以被直接使用。

  • dataproxy類需要實(shí)現(xiàn)readResolve方法,并且返回data對象。所以當(dāng)一個data類被反序列化的時候,內(nèi)部的dataproxy會被反序列化,之后就會調(diào)用readResolve方法,返回data對象給我們

  • 最后實(shí)現(xiàn)readObject方法,并且拋出異常,當(dāng)攻擊者想要改變data對象的流的時候,就會被這個方法拋出異常。

我們寫一個測試類,來測試這個代理模式是否可用

import java.io.IOException;

import SerializationUtil;

public class SerializationProxyTest {

    public static void main(String[] args) {
        String fileName = "data.ser";
        
        Data data = new Data("Pankaj");
        
        try {
            SerializationUtil.serialize(data, fileName);
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        try {
            Data newData = (Data) SerializationUtil.deserialize(fileName);
            System.out.println(newData);
        } catch (ClassNotFoundException | IOException e) {
            e.printStackTrace();
        }
    }

}

運(yùn)行結(jié)果

Data{data=Pankaj}

如果你打開data.ser文件,你會發(fā)現(xiàn)dataproxy對象被存在文件中。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • JAVA序列化機(jī)制的深入研究 對象序列化的最主要的用處就是在傳遞,和保存對象(object)的時候,保證對象的完整...
    時待吾閱讀 10,933評論 0 24
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,779評論 18 399
  • 對象的創(chuàng)建與銷毀 Item 1: 使用static工廠方法,而不是構(gòu)造函數(shù)創(chuàng)建對象:僅僅是創(chuàng)建對象的方法,并非Fa...
    孫小磊閱讀 2,031評論 0 3
  • 一、 序列化和反序列化概念 Serialization(序列化)是一種將對象以一連串的字節(jié)描述的過程;反序列化de...
    步積閱讀 1,458評論 0 10
  • 1.隱私權(quán)限設(shè)置 在info.plist里面加對應(yīng)的key-value,value對應(yīng)的是顯示的文字,key是對應(yīng)...
    CRAZYBADAM閱讀 492評論 0 0