JDK序列化分析

1. Jdk序列化舉例

1.1 JDK序列化簡介

序列化是把實體對象狀態按照一定的格式寫入到有序字節流,反序列化就是從有序字節流重建對象,恢復對象狀態。java序列化是指把java對象轉換爲字節序列的過程,而java反序列化是指把字節序列恢復爲java對象的過程。

1.2 JDK序列化示例

public class Student implements Serializable {

    private String id;

    private String name;

    public Student() {
    }

    public Student(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

public class JdkSerialize {

    public static void serializePerson(Student student) throws FileNotFoundException, IOException {
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(
                new File("/Users/zhuqiuhui/Desktop/student.txt")));
        oo.writeObject(student);
        oo.close();
    }

    public static Student deserializePerson() throws IOException, Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("/Users/zhuqiuhui/Desktop/student.txt")));
        Student student = (Student) ois.readObject();
        return student;
    }
}

public static void main(String[] args) {
  Student student = new Student();
  student.setId("123");
  student.setName("方辰");

  try {
    // 序列化
    JdkSerialize.serializePerson(student);

    // 反序列化
    Student serializeStudent = JdkSerialize.deserializePerson();
    System.out.println(serializeStudent.getName());
  } catch (Exception e) {
    e.printStackTrace();
  }

}

1.3 JDK序列化注意點

  • 序列化對象要實現Serializable接口

  • 若序列化對象增加字段,需要要序列化對象上增加固定的serialVersionUID。否則在反序列化過程中有可能報出以下錯誤:

在這裏插入圖片描述

(比如說上面示例,若Student對象序列化後,再修改Student類,然後再進行反序列化就會報上述異常)。

2. JDK序列化分析

2.1 JDK序列化爲什麼需要 serialVersionUID ?

Serializable接口中文檔描述如下:

If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification. However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassExceptions during deserialization. Therefore, to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit serialVersionUID value. It is also strongly advised that explicit serialVersionUID declarations use the private modifier where possible, since such declarations apply only to the immediately declaring class--serialVersionUID fields are not useful as inherited members. Array classes cannot declare an explicit serialVersionUID, so they always have the default computed value, but the requirement for matching serialVersionUID values is waived for array classes.

描述重點有兩點:

  • 實現Serializable接口的對象推薦顯示指定 serialVersionUID(私有),否則可能報InvalidClassExceptions。

2.2 JDK序列化與反序列化源碼分析

先看對象的序列化的過程中生成serialVersionUID的代碼,如下(serialVersionUID作爲類描述符信息序列化爲存儲對象的):

// java.io.ObjectStreamClass對象中
void writeNonProxy(ObjectOutputStream out) throws IOException {
  out.writeUTF(name);    // 寫入類名
  out.writeLong(getSerialVersionUID());   // 寫入類的序列號

  byte flags = 0;   // 類的標記
  if (externalizable) {
    flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
    int protocol = out.getProtocolVersion();
    if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
      flags |= ObjectStreamConstants.SC_BLOCK_DATA;
    }
  } else if (serializable) {
    flags |= ObjectStreamConstants.SC_SERIALIZABLE;
  }
  if (hasWriteObjectData) {
    flags |= ObjectStreamConstants.SC_WRITE_METHOD;
  }
  if (isEnum) {
    flags |= ObjectStreamConstants.SC_ENUM;
  }
  out.writeByte(flags);  // 寫入類的標記

  out.writeShort(fields.length);  // 寫入對象的字段數量
  for (int i = 0; i < fields.length; i++) {
    ObjectStreamField f = fields[i];
    out.writeByte(f.getTypeCode());
    out.writeUTF(f.getName());
    if (!f.isPrimitive()) {
      out.writeTypeString(f.getTypeString());
    }
  }
}

/**
** 在創建ObjectStreamClass時,會從類實例中取出“serialVersionUID”屬性的值設置到ObjectStreamClass的suid中
**/
public long getSerialVersionUID() {
  if (suid == null) {
    suid = AccessController.doPrivileged(
      new PrivilegedAction<Long>() {
        public Long run() {
          return computeDefaultSUID(cl);  // 沒有設置會創建一個默認的serialVersionUID(對類名,接口名,方法和屬性的名稱、修飾符以及描述符的64位哈希值,另外static/transient類型成員變量、私有方法都不參與Hash值計算。)
        }
      }
    );
  }
  return suid.longValue();
}

再看反序列化過程,即java.io.ObjectStreamClass#initNonProxy方法,如下:

/**
** ObjectStreamClass model:被序列化的對象(如存儲文件)生成的
** cl:根據model(上面對象)的className獲取到的類的class文件
*/
void initNonProxy(ObjectStreamClass model,
                  Class<?> cl,
                  ClassNotFoundException resolveEx,
                  ObjectStreamClass superDesc) throws InvalidClassException {
  
  long suid = Long.valueOf(model.getSerialVersionUID()); // model從
  
  ObjectStreamClass osc = null;
  if (cl != null) {
    osc = lookup(cl, true);
    if (osc.isProxy) {
      throw new InvalidClassException(
        "cannot bind non-proxy descriptor to a proxy class");
    }
    if (model.isEnum != osc.isEnum) {
      throw new InvalidClassException(model.isEnum ?
                                      "cannot bind enum descriptor to a non-enum class" :
                                      "cannot bind non-enum descriptor to an enum class");
    }

    // suid是要被序列化的對象的serialVersionUID(如存儲文件),ObjectStreamClass osc是cl生成的(從類文件找到的最新的)
    if (model.serializable == osc.serializable &&
        !cl.isArray() &&
        suid != osc.getSerialVersionUID()) {
      throw new InvalidClassException(osc.name,
                                      "local class incompatible: " +
                                      "stream classdesc serialVersionUID = " + suid +
                                      ", local class serialVersionUID = " +
                                      osc.getSerialVersionUID());
    }

    if (!classNamesEqual(model.name, osc.name)) {
      throw new InvalidClassException(osc.name,
                                      "local class name incompatible with stream class " +
                                      "name \"" + model.name + "\"");
    }
    //.......
}

2.3 結論

  • 在序列化過程,若不指定serialVersionUID,jdk會根據序列化對象的類信息生成默認的serialVersionUID,同時設置到類的描述符中。
  • 在反序列化過程中,讀取到的序列化對象流會和最新的類的serialVersionUID對比,若不相等,則拋出InvalidClassExceptions異常。
  • 在JDK序列化過程中儘量對序列化對象設置固定的serialVersionUID。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章