Java序列化示例教程

jopen 9年前發布 | 19K 次閱讀 Java Java開發

Java序列化是在JDK 1.1中引入的,是Java內核的重要特性之一。Java序列化API允許我們將一個對象轉換為流,并通過網絡發送,或將其存入文件或數據庫以便未來使用,反序列化則是將對象流轉換為實際程序中使用的Java對象的過程。Java同步化過程乍看起來很好用,但它會帶來一些瑣碎的安全性和完整性問題,在文章的后面部分我們會涉及到,以下是本教程涉及的主題。

  1. Java序列化接口
  2. 使用序列化和serialVersionUID進行類重構
  3. Java外部化接口
  4. Java序列化方法
  5. 序列化結合繼承
  6. 序列化代理模式

Java序列化接口

如果你希望一個類對象是可序列化的,你所要做的是實現java.io.Serializable接口。序列化一種標記接口,不需要實現任何字段和方法,這就像是一種選擇性加入的處理,通過它可以使類對象成為可序列化的對象。

序列化處理是通過ObjectInputStream和ObjectOutputStream實現的,因此我們所要做的是基于它們進行一層封裝,要么將其保存為文件,要么將其通過網絡發送。我們來看一個簡單的序列化示例。

package com.journaldev.serialization;

import java.io.Serializable;

public class Employee implements Serializable {

//  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;
//  }

}

注意一下,這是一個簡單的java bean,擁有一些屬性以及getter-setter方法,如果你想要某個對象屬性不被序列化成流,你可以使用transient關鍵字,正如示例中我在salary變量上的做法那樣。

現在我們假設需要把我們的對象寫入文件,之后從相同的文件中將其反序列化,因此我們需要一些工具方法,通過使用ObjectInputStream和ObjectOutputStream來達到序列化的目的。

package com.journaldev.serialization;

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 pankaj
 * 
 */
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();
    }

}

注意一下,方法的參數是Object,它是任何Java類的基類,這樣寫法以一種很自然的方式保證了通用性。

現在我們來寫一個測試程序,看一下Java序列化的實戰。

package com.journaldev.serialization;

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);
    }

}

運行以上測試程序,可以得到以下輸出。

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

由于salary是一個transient變量,它的值不會被存入文件中,因此也不會在新的對象中被恢復。類似的,靜態變量的值也不會被序列化,因為他們是屬于類而非對象的。

使用序列化和serialVersionUID進行類重構

Java序列化允許java類中的一些變化,如果他們可以被忽略的話。一些不會影響到反序列化處理的變化有:

  • 在類中添加一些新的變量。
  • 將變量從transient轉變為非tansient,對于序列化來說,就像是新加入了一個變量而已。
  • 將變量從靜態的轉變為非靜態的,對于序列化來說,就也像是新加入了一個變量而已。

不過這些變化要正常工作,java類需要具有為該類定義的serialVersionUID,我們來寫一個測試類,只對之前測試類已經生成的序列化文件進行反序列化。

package com.journaldev.serialization;

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);

    }

}

現在,在Employee類中去掉”password”變量的注釋和它的getter-setter方法,運行。你會得到以下異常。

java.io.InvalidClassException: com.journaldev.serialization.Employee; local class incompatible: stream classdesc serialVersionUID = -6470090944414208496, local class serialVersionUID = -6234198221249432383
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369)
    at com.journaldev.serialization.SerializationUtil.deserialize(SerializationUtil.java:22)
    at com.journaldev.serialization.DeserializationTest.main(DeserializationTest.java:13)
empNew Object::null

原因很顯然,上一個類和新類的serialVersionUID是不同的,事實上如果一個類沒有定義serialVersionUID,它會自動計算出來并分配給該類。Java使用類變量、方法、類名稱、包,等等來產生這個特殊的長數。如果你在任何一個IDE上工作,你都會得到警告“可序列化類Employee沒有定義一個靜態的final的serialVersionUID,類型為long”。

我們可以使用java工具”serialver”來產生一個類的serialVersionUID,對于Employee類,可以執行以下命令。

SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee

記住,從程序本身生成序列版本并不是必須的,我們可以根據需要指定值,這個值的作用僅僅是告知反序列化處理機制,新的類是相同的類的新版本,應該進行可能的反序列化處理。

舉個例子,在Employee類中僅僅將serialVersionUID字段的注釋去掉,運行SerializationTest程序。現在再將Employee類中的password字段的注釋去掉,運行DeserializationTest程序,你會看到對象流被成功地反序列化了,因為Employee類中的變動與序列化處理是相容的。

Java外部化接口

如果你在序列化處理中留個心,你會發現它是自動處理的。有時候我們想要去隱藏對象數據,來保持它的完整性,可以通過實現java.io.Externalizable接口,并提供writeExternal()和readExternal()方法的實現,它們被用于序列化處理。

package com.journaldev.externalization;

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;
    }

}

注意,在將其轉換為流之前,我已經更改了字段的值,之后讀取時會得到這些更改,通過這種方式,可以在某種程度上保證數據的完整性,我們可以在讀取流數據之后拋出異常,表明完整性檢查失敗。來看一個測試程序。

package com.journaldev.externalization;

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();
        }

    }

}

運行以上測試程序,可以得到以下輸出。

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

那么哪個方式更適合被用來做序列化處理呢?實際上使用序列化接口更好,當你看到這篇教程的末尾時,你會知道原因的。

Java序列化方法

我們已經看到了,java的序列化是自動的,我們所要做的僅僅是實現序列化接口,其實現已經存在于ObjectInputStream和ObjectOutputStream類中了。不過如果我們想要更改存儲數據的方式,比如說在對象中含有一些敏感信息,在存儲/獲取它們之前我們要進行加密/解密,這該怎么辦呢?這就是為什么在類中我們擁有四種方法,能夠改變序列化行為。

如果以下方法在類中存在,它們就會被用于序列化處理。

  1. readObject(ObjectInputStream ois):如果這個方法存在,ObjectInputStream readObject()方法會調用該方法從流中讀取對象。
  2. writeObject(ObjectOutputStream oos):如果這個方法存在,ObjectOutputStream writeObject()方法會調用該方法從流中寫入對象。一種普遍的用法是隱藏對象的值來保證完整性。
  3. Object writeReplace():如果這個方法存在,那么在序列化處理之后,該方法會被調用并將返回的對象序列化到流中。
  4. Object readResolve():如果這個方法存在,那么在序列化處理之后,該方法會被調用并返回一個最終的對象給調用程序。一種使用方法是在序列化類中實現單例模式,你可以從序列化和單例中讀到更多知識。

通常情況下,當實現以上方法時,應該將其設定為私有類型,這樣子類就無法覆蓋它們了,因為它們本來就是為了序列化而建立的,設定為私有類型能避免一些安全性問題。

序列化結合繼承

有時候我們需要對一個沒有實現序列化接口的類進行擴展,如果依賴于自動化的序列化行為,而一些狀態是父類擁有的,那么它們將不會被轉換為流,因此以后也無法獲取。

在此,readObject()和writeObject()就可以派上大用處了,通過提供它們的實現,我們可以將父類的狀態存入流中,以便今后獲取。我們來看一下實戰。

package com.journaldev.serialization.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;
    }

}

父類是一個簡單的java bean,沒有實現序列化接口。

package com.journaldev.serialization.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");
    }

}

注意,將額外數據寫入流和讀取流的順序應該是一致的,我們可以在讀與寫之中添加一些邏輯,使其更安全。

同時還需要注意,這個類實現了ObjectInputValidation接口,通過實現validateObject()方法,可以添加一些業務驗證來確保數據完整性沒有遭到破壞。

以下通過編寫一個測試類,看一下我們是否能夠從序列化的數據中獲取父類的狀態。

package com.journaldev.serialization.inheritance;

import java.io.IOException;

import com.journaldev.serialization.SerializationUtil;

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();
        }
    }

}

運行以上測試程序,可以得到以下輸出。

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

因此通過這種方式,可以序列化父類的狀態,即便它沒有實現序列化接口。當父類是一個我們無法改變的第三方的類,這個策略就有用武之地了。

序列化代理模式

Java序列化也帶來了一些嚴重的誤區,比如:

  • 類的結構無法大量改變,除非中斷序列化處理,因此即便我們之后已經不需要某些變量了,我們也需要保留它們,僅僅是為了向后兼容。
  • 序列化會導致巨大的安全性危機,一個攻擊者可以更改流的順序,繼而對系統造成傷害。舉個例子,用戶角色被序列化了,攻擊者可以更改流的值為admin,再執行惡意代碼。

序列化代理模式是一種使序列化能達到極高安全性的方式,在這個模式下,一個內部的私有靜態類被用作序列化的代理類,該類的設計目的是用于保留主類的狀態。這個模式的實現需要合理實現readResolve()和writeReplace()方法。

讓我們先來寫一個類,實現了序列化代碼模式,之后再對其進行分析,以便更好的理解原理。

package com.journaldev.serialization.proxy;

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類都應該實現序列化接口。
  • DataProxy應該能夠保留Data對象的狀態。
  • DataProxy是一個內部的私有靜態類,因此其他類無法訪問它。
  • DataProxy應該有一個單獨的構造方法,接收Data作為參數。
  • Data類應該提供writeReplace()方法,返回DataProxy實例,這樣當Data對象被序列化時,返回的流是屬于DataProxy類的,不過DataProxy類在外部是不可見的,所有它不能被直接使用。
  • DataProxy應該實現readResolve()方法,返回Data對象,這樣當Data類被反序列化時,在內部其實是DataProxy類被反序列化了,之后它的readResolve()方法被調用,我們得到了Data對象。
  • 最后,在Data類中實現readObject()方法,拋出InvalidObjectException異常,防止黑客通過偽造Data對象的流并對其進行解析,繼而執行攻擊。

我們來寫一個小測試,檢查一下這樣的實現是否能工作。

package com.journaldev.serialization.proxy;

import java.io.IOException;

import com.journaldev.serialization.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();
        }
    }

}

運行以上測試程序,可以得到以下輸出。

Data{data=Pankaj}

如果你打開data.ser文件,可以看到DataProxy對象已經被作為流存入了文件中。

這就是Java序列化的所有內容,看上去很簡單但我們應當謹慎地使用它,通常來說,最好不要依賴于默認實現。你可以從上面的鏈接中下載項目,玩一玩,這能讓你學到更多。

原文鏈接: Java Serialization Example Tutorial, Serializable, serialVersionUID 翻譯: ImportNew.com - Justin Wu
譯文鏈接: http://www.importnew.com/14465.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!