Java 序列化 JDK序列化總結
@author ixenos
Java序列化是在JDK 1.1中引入的,是Java內核的重要特性之一。Java序列化API允許我們將一個對象轉換為流,並通過網絡發送,或將其存入文件或數據庫以便未來使用,反序列化則是將對象流轉換為實際程序中使用的Java對象的過程。Java同步化過程乍看起來很好用,但它會帶來一些瑣碎的安全性和完整性問題,在文章的后面部分我們會涉及到,以下是本教程涉及的主題。
- Java序列化接口
- 使用序列化和serialVersionUID進行類重構
- Java外部化接口
- Java序列化方法
- 序列化結合繼承
- 序列化代理模式
Java序列化接口 java.io.Serializable
如果你希望一個類對象是可序列化的,你所要做的是實現java.io.Serializable接口。序列化一種標記接口,不需要實現任何字段和方法,這就像是一種選擇性加入的處理,通過它可以使類對象成為可序列化的對象。
序列化處理是通過ObjectInputStream和ObjectOutputStream實現的,因此我們所要做的是基於它們進行一層封裝,要么將其保存為文件,要么將其通過網絡發送。我們來看一個簡單的序列化示例。
1 package com.journaldev.serialization; 2 3 import java.io.Serializable; 4 5 public class Employee implements Serializable { 6 7 // private static final long serialVersionUID = -6470090944414208496L; 8 9 private String name; 10 private int id; 11 transient private int salary; 12 // private String password; 13 14 @Override 15 public String toString(){ 16 return "Employee{name="+name+",id="+id+",salary="+salary+"}"; 17 } 18 19 //getter and setter methods 20 public String getName() { 21 return name; 22 } 23 24 public void setName(String name) { 25 this.name = name; 26 } 27 28 public int getId() { 29 return id; 30 } 31 32 public void setId(int id) { 33 this.id = id; 34 } 35 36 public int getSalary() { 37 return salary; 38 } 39 40 public void setSalary(int salary) { 41 this.salary = salary; 42 } 43 44 // public String getPassword() { 45 // return password; 46 // } 47 // 48 // public void setPassword(String password) { 49 // this.password = password; 50 // } 51 52 }
注意一下,這是一個簡單的java bean,擁有一些屬性以及getter-setter方法,如果你想要某個對象屬性不被序列化成流,你可以使用transient關鍵字,正如示例中我在salary變量上的做法那樣。
現在我們假設需要把我們的對象寫入文件,之后從相同的文件中將其反序列化,因此我們需要一些工具方法,通過使用ObjectInputStream和ObjectOutputStream來達到序列化的目的。
1 package com.journaldev.serialization; 2 3 import java.io.FileInputStream; 4 import java.io.FileOutputStream; 5 import java.io.IOException; 6 import java.io.ObjectInputStream; 7 import java.io.ObjectOutputStream; 8 9 /** 10 * A simple class with generic serialize and deserialize method implementations 11 * 12 * @author pankaj 13 * 14 */ 15 public class SerializationUtil { 16 17 // deserialize to Object from given file 18 public static Object deserialize(String fileName) throws IOException, 19 ClassNotFoundException { 20 FileInputStream fis = new FileInputStream(fileName); 21 ObjectInputStream ois = new ObjectInputStream(fis); 22 Object obj = ois.readObject(); 23 ois.close(); 24 return obj; 25 } 26 27 // serialize the given object and save it to file 28 public static void serialize(Object obj, String fileName) 29 throws IOException { 30 FileOutputStream fos = new FileOutputStream(fileName); 31 ObjectOutputStream oos = new ObjectOutputStream(fos); 32 oos.writeObject(obj); 33 34 fos.close(); 35 } 36 37 }
注意一下,方法的參數是Object,它是任何Java類的基類,這樣寫法以一種很自然的方式保證了通用性。
現在我們來寫一個測試程序,看一下Java序列化的實戰。
1 package com.journaldev.serialization; 2 3 import java.io.IOException; 4 5 public class SerializationTest { 6 7 public static void main(String[] args) { 8 String fileName="employee.ser"; 9 Employee emp = new Employee(); 10 emp.setId(100); 11 emp.setName("Pankaj"); 12 emp.setSalary(5000); 13 14 //serialize to file 15 try { 16 SerializationUtil.serialize(emp, fileName); 17 } catch (IOException e) { 18 e.printStackTrace(); 19 return; 20 } 21 22 Employee empNew = null; 23 try { 24 empNew = (Employee) SerializationUtil.deserialize(fileName); 25 } catch (ClassNotFoundException | IOException e) { 26 e.printStackTrace(); 27 } 28 29 System.out.println("emp Object::"+emp); 30 System.out.println("empNew Object::"+empNew); 31 } 32 33 }
運行以上測試程序,可以得到以下輸出。
1 emp Object::Employee{name=Pankaj,id=100,salary=5000} 2 empNew Object::Employee{name=Pankaj,id=100,salary=0}
由於salary是一個transient變量,它的值不會被存入文件中,因此也不會在新的對象中被恢復。類似的,靜態變量的值也不會被序列化,因為他們是屬於類而非對象的。
使用序列化和serialVersionUID進行類重構
Java序列化允許java類中的一些變化,如果他們可以被忽略的話。一些不會影響到反序列化處理的變化有:
- 在類中添加一些新的變量。
- 將變量從transient轉變為非tansient,對於序列化來說,就像是新加入了一個變量而已。
- 將變量從靜態的轉變為非靜態的,對於序列化來說,就也像是新加入了一個變量而已。
不過這些變化要正常工作,java類需要具有為該類定義的serialVersionUID,我們來寫一個測試類,只對之前測試類已經生成的序列化文件進行反序列化。
1 package com.journaldev.serialization; 2 3 import java.io.IOException; 4 5 public class DeserializationTest { 6 7 public static void main(String[] args) { 8 9 String fileName="employee.ser"; 10 Employee empNew = null; 11 12 try { 13 empNew = (Employee) SerializationUtil.deserialize(fileName); 14 } catch (ClassNotFoundException | IOException e) { 15 e.printStackTrace(); 16 } 17 18 System.out.println("empNew Object::"+empNew); 19 20 } 21 22 }
現在,在Employee類中去掉”password”變量的注釋和它的getter-setter方法,運行。你會得到以下異常。
1 java.io.InvalidClassException: com.journaldev.serialization.Employee; local class incompatible: stream classdesc serialVersionUID = -6470090944414208496, local class serialVersionUID = -6234198221249432383 2 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604) 3 at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601) 4 at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514) 5 at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750) 6 at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347) 7 at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369) 8 at com.journaldev.serialization.SerializationUtil.deserialize(SerializationUtil.java:22) 9 at com.journaldev.serialization.DeserializationTest.main(DeserializationTest.java:13) 10 empNew Object::null
原因很顯然,上一個類和新類的serialVersionUID是不同的,事實上如果一個類沒有定義serialVersionUID,它會自動計算出來並分配給該類。Java使用類變量、方法、類名稱、包,等等來產生這個特殊的長數。如果你在任何一個IDE上工作,你都會得到警告“可序列化類Employee沒有定義一個靜態的final的serialVersionUID,類型為long”。
我們可以使用java工具”serialver”來產生一個類的serialVersionUID,對於Employee類,可以執行以下命令。
1 SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee
記住,從程序本身生成序列版本並不是必須的,我們可以根據需要指定值,這個值的作用僅僅是告知反序列化處理機制,新的類是相同的類的新版本,應該進行可能的反序列化處理。
舉個例子,在Employee類中僅僅將serialVersionUID字段的注釋去掉,運行SerializationTest程序。現在再將Employee類中的password字段的注釋去掉,運行DeserializationTest程序,你會看到對象流被成功地反序列化了,因為Employee類中的變動與序列化處理是相容的。
Java外部化接口 java.io.Externalizable
如果你在序列化處理中留個心,你會發現它是自動處理的。有時候我們想要去隱藏對象數據,來保持它的完整性,可以通過實現java.io.Externalizable接口,並提供writeExternal()和readExternal()方法的實現,它們被用於序列化處理。
1 package com.journaldev.externalization; 2 3 import java.io.Externalizable; 4 import java.io.IOException; 5 import java.io.ObjectInput; 6 import java.io.ObjectOutput; 7 8 public class Person implements Externalizable{ 9 10 private int id; 11 private String name; 12 private String gender; 13 14 @Override 15 public void writeExternal(ObjectOutput out) throws IOException { 16 out.writeInt(id); 17 out.writeObject(name+"xyz"); 18 out.writeObject("abc"+gender); 19 } 20 21 @Override 22 public void readExternal(ObjectInput in) throws IOException, 23 ClassNotFoundException { 24 id=in.readInt(); 25 //read in the same order as written 26 name=(String) in.readObject(); 27 if(!name.endsWith("xyz")) throw new IOException("corrupted data"); 28 name=name.substring(0, name.length()-3); 29 gender=(String) in.readObject(); 30 if(!gender.startsWith("abc")) throw new IOException("corrupted data"); 31 gender=gender.substring(3); 32 } 33 34 @Override 35 public String toString(){ 36 return "Person{id="+id+",name="+name+",gender="+gender+"}"; 37 } 38 public int getId() { 39 return id; 40 } 41 42 public void setId(int id) { 43 this.id = id; 44 } 45 46 public String getName() { 47 return name; 48 } 49 50 public void setName(String name) { 51 this.name = name; 52 } 53 54 public String getGender() { 55 return gender; 56 } 57 58 public void setGender(String gender) { 59 this.gender = gender; 60 } 61 62 }
注意,在將其轉換為流之前,我已經更改了字段的值,之后讀取時會得到這些更改,通過這種方式,可以在某種程度上保證數據的完整性,我們可以在讀取流數據之后拋出異常,表明完整性檢查失敗。
來看一個測試程序。
1 package com.journaldev.externalization; 2 3 import java.io.FileInputStream; 4 import java.io.FileOutputStream; 5 import java.io.IOException; 6 import java.io.ObjectInputStream; 7 import java.io.ObjectOutputStream; 8 9 public class ExternalizationTest { 10 11 public static void main(String[] args) { 12 13 String fileName = "person.ser"; 14 Person person = new Person(); 15 person.setId(1); 16 person.setName("Pankaj"); 17 person.setGender("Male"); 18 19 try { 20 FileOutputStream fos = new FileOutputStream(fileName); 21 ObjectOutputStream oos = new ObjectOutputStream(fos); 22 oos.writeObject(person); 23 oos.close(); 24 } catch (IOException e) { 25 // TODO Auto-generated catch block 26 e.printStackTrace(); 27 } 28 29 FileInputStream fis; 30 try { 31 fis = new FileInputStream(fileName); 32 ObjectInputStream ois = new ObjectInputStream(fis); 33 Person p = (Person)ois.readObject(); 34 ois.close(); 35 System.out.println("Person Object Read="+p); 36 } catch (IOException | ClassNotFoundException e) { 37 e.printStackTrace(); 38 } 39 40 } 41 42 }
運行以上測試程序,可以得到以下輸出。
1 Person Object Read=Person{id=1,name=Pankaj,gender=Male}
那么哪個方式更適合被用來做序列化處理呢?實際上使用序列化接口更好,當你看到這篇教程的末尾時,你會知道原因的。
Serializable和Externalizable的區別與聯系
1、Externalizable繼承自Serializable;Externalizable 實例也可以通過 Serializable 接口中記錄的 writeReplace 和 readResolve 方法來指派一個替代對象
2、實現Externalizable接口的類必須有默認構造方法。在讀入可外部化的(Externalizable)類時,對象流將先用無參構造器創建一個對象,然后調用readExternal方法按writeExternal定義的機制反序列化;
3、若要完全控制某一對象及其超類型的流格式和內容,則它要實現 Externalizable 接口的 writeExternal 和 readExternal 方法。這些方法必須顯式與超類型進行協調以保存其狀態。這些方法將代替定制的 writeObject 和 readObject 方法實現;write/readExternal要對包括超類數據在內的整個對象的存儲和恢復負全責,而Serializable在流中僅僅記錄該對象所屬的類的屬性狀態;
1 public void readExternal(ObjectInput in) throws IOException, 2 ClassNotFoundException { 3 name = (String) in.readObject(); 4 password = (String) in.readObject(); 5 } 6 7 public void writeExternal(ObjectOutput out) throws IOException { 8 out.writeObject( name); 9 out.writeObject( password); 10 }
但是,如果多個對象a是多次被序列化的,在 反序列后對象b會被反序列化多次,因為反序列化時都調用構造器(Externalizable調用無參構造器)在堆中生成新對象,內存地址都是不同的,即多個a對象的屬性b是不一樣的。
6、Serialization 對象將使用 Serializable 和 Externalizable 接口。對象持久性機制也可以使用它們。要存儲的每個對象都需要檢測是否支持 Externalizable 接口。
a) 如果對象支持 Externalizable,則調用 writeExternal 方法。如果對象不支持 Externalizable 但實現了 Serializable,則使用 ObjectOutputStream 保存該對象。
b) 在重構 Externalizable 對象時,先使用無參數的公共構造方法創建一個實例,然后調用 readExternal 方法。通過從 ObjectInputStream 中讀取 Serializable 對象可以恢復這些對象。
Java序列化方法 java.io.Serializable
我們已經看到了,java的序列化是自動的,我們所要做的僅僅是實現序列化接口,其實現已經存在於ObjectInputStream和ObjectOutputStream類中了。不過如果我們想要更改存儲數據的方式,比如說在對象中含有一些敏感信息,在存儲/獲取它們之前我們要進行加密/解密,這該怎么辦呢?這就是為什么在類中我們擁有四種方法,能夠改變序列化行為。
如果以下方法在被序列化類中存在,它們就會被用於序列化處理。
- readObject(ObjectInputStream ois):如果這個方法存在,ObjectInputStream readObject()方法會調用該方法從流中讀取對象。
- writeObject(ObjectOutputStream oos):如果這個方法存在,ObjectOutputStream writeObject()方法會調用該方法從流中寫入對象。一種普遍的用法是隱藏對象的值來保證完整性。
- Object writeReplace():如果這個方法存在,那么在序列化處理之后,該方法會被調用並將返回的對象序列化到流中。
- Object readResolve():如果這個方法存在,那么在序列化處理之后,該方法會被調用並返回一個最終的對象給調用程序(keyijinxing)。一種使用方法是在序列化類中實現單例模式,你可以從序列化和單例中讀到更多知識。此方法返回的對象,會被作為readOjbect的返回值(即使readObject方法定義並沒有返回任何對象)
通常情況下,當實現以上方法時,應該將其設定為私有類型,這樣子類就無法覆蓋它們了,因為它們本來就是為了序列化而建立的,設定為私有類型能避免一些安全性問題。
readObejct和writeObject的具體示例:Java Object 對象序列化和反序列化
writeReplace和readResolve的具體示例:Java Object 序列化與單例模式 以及本文的 序列化代理模式
序列化結合繼承
有時候我們需要對一個沒有實現序列化接口的類進行擴展,如果依賴於自動化的序列化行為,而一些狀態是父類擁有的,那么它們將不會被轉換為流,因此以后也無法獲取。
在此,readObject()和writeObject()就可以派上大用處了,通過提供它們的實現,我們可以將父類的狀態存入流中,以便今后獲取。我們來看一下實戰。
1 package com.journaldev.serialization.inheritance; 2 3 public class SuperClass { 4 5 private int id; 6 private String value; 7 8 public int getId() { 9 return id; 10 } 11 public void setId(int id) { 12 this.id = id; 13 } 14 public String getValue() { 15 return value; 16 } 17 public void setValue(String value) { 18 this.value = value; 19 } 20 21 }
父類是一個簡單的java bean,沒有實現序列化接口。
1 package com.journaldev.serialization.inheritance; 2 3 import java.io.IOException; 4 import java.io.InvalidObjectException; 5 import java.io.ObjectInputStream; 6 import java.io.ObjectInputValidation; 7 import java.io.ObjectOutputStream; 8 import java.io.Serializable; 9 10 public class SubClass extends SuperClass implements Serializable, ObjectInputValidation{ 11 12 private static final long serialVersionUID = -1322322139926390329L; 13 14 private String name; 15 16 public String getName() { 17 return name; 18 } 19 20 public void setName(String name) { 21 this.name = name; 22 } 23 24 @Override 25 public String toString(){ 26 return "SubClass{id="+getId()+",value="+getValue()+",name="+getName()+"}"; 27 } 28 29 //adding helper method for serialization to save/initialize super class state 30 private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{ 31 ois.defaultReadObject(); 32 33 //注意讀和寫的順序要一致,反序列化出值,再賦給父類屬性 34 setId(ois.readInt()); 35 setValue((String) ois.readObject()); 36 37 } 38 39 private void writeObject(ObjectOutputStream oos) throws IOException{ 40 oos.defaultWriteObject(); 41 //調用父類的getId獲得值單獨寫到序列化流中 42 oos.writeInt(getId()); 43 oos.writeObject(getValue()); 44 } 45 46 @Override 47 public void validateObject() throws InvalidObjectException { 48 //validate the object here 49 if(name == null || "".equals(name)) throw new InvalidObjectException("name can't be null or empty"); 50 if(getId() <=0) throw new InvalidObjectException("ID can't be negative or zero"); 51 } 52 53 }
注意,將額外數據寫入流和讀取流的順序應該是一致的,我們可以在讀與寫之中添加一些邏輯,使其更安全。
同時還需要注意,這個類實現了ObjectInputValidation接口,通過實現validateObject()方法,可以添加一些業務驗證來確保數據完整性沒有遭到破壞。
以下通過編寫一個測試類,看一下我們是否能夠從序列化的數據中獲取父類的狀態。
1 package com.journaldev.serialization.inheritance; 2 3 import java.io.IOException; 4 5 import com.journaldev.serialization.SerializationUtil; 6 7 public class InheritanceSerializationTest { 8 9 public static void main(String[] args) { 10 String fileName = "subclass.ser"; 11 12 SubClass subClass = new SubClass(); 13 subClass.setId(10); 14 subClass.setValue("Data"); 15 subClass.setName("Pankaj"); 16 17 try { 18 SerializationUtil.serialize(subClass, fileName); 19 } catch (IOException e) { 20 e.printStackTrace(); 21 return; 22 } 23 24 try { 25 SubClass subNew = (SubClass) SerializationUtil.deserialize(fileName); 26 System.out.println("SubClass read = "+subNew); 27 } catch (ClassNotFoundException | IOException e) { 28 e.printStackTrace(); 29 } 30 } 31 32 }
運行以上測試程序,可以得到以下輸出。
1 SubClass read = SubClass{id=10,value=Data,name=Pankaj}
因此通過這種方式,可以序列化父類的狀態,即便它沒有實現序列化接口。當父類是一個我們無法改變的第三方的類,這個策略就有用武之地了。
序列化代理模式
Java序列化也帶來了一些嚴重的誤區,比如:
- 類的結構無法大量改變,除非中斷序列化處理,因此即便我們之后已經不需要某些變量了,我們也需要保留它們,僅僅是為了向后兼容。
- 序列化會導致巨大的安全性危機,一個攻擊者可以更改流的順序,繼而對系統造成傷害。舉個例子,用戶角色被序列化了,攻擊者可以更改流的值為admin,再執行惡意代碼。
序列化代理模式是一種使序列化能達到極高安全性的方式,在這個模式下,一個內部的私有靜態類被用作序列化的代理類,該類的設計目的是用於保留主類的狀態。這個模式的實現需要合理實現readResolve()和writeReplace()方法。
讓我們先來寫一個類,實現了序列化代碼模式,之后再對其進行分析,以便更好的理解原理。
1 package com.journaldev.serialization.proxy; 2 3 import java.io.InvalidObjectException; 4 import java.io.ObjectInputStream; 5 import java.io.Serializable; 6 7 public class Data implements Serializable{ 8 9 private static final long serialVersionUID = 2087368867376448459L; 10 11 private String data; 12 13 public Data(String d){ 14 this.data=d; 15 } 16 17 public String getData() { 18 return data; 19 } 20 21 public void setData(String data) { 22 this.data = data; 23 } 24 25 @Override 26 public String toString(){ 27 return "Data{data="+data+"}"; 28 } 29 30 //serialization proxy class 31 private static class DataProxy implements Serializable{ 32 33 private static final long serialVersionUID = 8333905273185436744L; 34 35 private String dataProxy; 36 private static final String PREFIX = "ABC"; 37 private static final String SUFFIX = "DEFG"; 38 39 public DataProxy(Data d){ 40 //obscuring data for security 41 this.dataProxy = PREFIX + d.data + SUFFIX; 42 } 43 44 private Object readResolve() throws InvalidObjectException { 45 if(dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)){ 46 return new Data(dataProxy.substring(3, dataProxy.length() -4)); 47 }else throw new InvalidObjectException("data corrupted"); 48 } 49 50 } 51 52 //replacing serialized object to DataProxy object 53 private Object writeReplace(){ 54 return new DataProxy(this); 55 } 56 57 private void readObject(ObjectInputStream ois) throws InvalidObjectException{ 58 throw new InvalidObjectException("Proxy is not used, something fishy"); 59 } 60 }
- Data和DataProxy類都應該實現序列化接口。
- DataProxy應該能夠保留Data對象的狀態。
- DataProxy是一個內部的私有靜態類,因此其他類無法訪問它。
- DataProxy應該有一個單獨的構造方法,接收Data作為參數。
- Data類應該提供writeReplace()方法,返回DataProxy實例,這樣當Data對象被序列化時,返回的流是屬於DataProxy類的,不過DataProxy類在外部是不可見的,所有它不能被直接使用。
- DataProxy應該實現readResolve()方法,返回Data對象,這樣當Data類被反序列化時,在內部其實是DataProxy類被反序列化了,之后它的readResolve()方法被調用,我們得到了Data對象。
- 最后,在Data類中實現readObject()方法,拋出InvalidObjectException異常,防止黑客通過偽造Data對象的流並對其進行解析,繼而執行攻擊。
我們來寫一個小測試,檢查一下這樣的實現是否能工作。
1 package com.journaldev.serialization.proxy; 2 3 import java.io.IOException; 4 5 import com.journaldev.serialization.SerializationUtil; 6 7 public class SerializationProxyTest { 8 9 public static void main(String[] args) { 10 String fileName = "data.ser"; 11 12 Data data = new Data("Pankaj"); 13 14 try { 15 SerializationUtil.serialize(data, fileName); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 20 try { 21 Data newData = (Data) SerializationUtil.deserialize(fileName); 22 System.out.println(newData); 23 } catch (ClassNotFoundException | IOException e) { 24 e.printStackTrace(); 25 } 26 } 27 28 }
運行以上測試程序,可以得到以下輸出。
1 Data{data=Pankaj
如果你打開data.ser文件,可以看到DataProxy對象已經被作為流存入了文件中。
這就是Java序列化的所有內容,看上去很簡單但我們應當謹慎地使用它,通常來說,最好不要依賴於默認實現。你可以從上面的鏈接中下載項目,玩一玩,這能讓你學到更多。
本文參考以下內容:
原文鏈接: journaldev 翻譯: ImportNew.com - Justin Wu
譯文鏈接: http://www.importnew.com/14465.html