本文目錄java
前言
Oracle 公司計劃廢除 Java 中的古董:序列化技術,由於它帶來了許多嚴重的安全問題(如序列化存儲安全、反序列化安全、傳輸安全等),據統計,至少有3分之1的漏洞是序列化帶來的,這也是 1997 年誕生序列化技術的一個巨大錯誤。可是,序列化技術如今在 Java 應用中無處不在,特別是如今的持久化框架和分佈式技術中,都須要利用序列化來傳輸對象,如:Hibernate、Mybatis、Java RMI、Dubbo等,即對象要存儲或者傳輸都不可避免要用到序列化技術,因此刪除序列化技術將是一個長期的計劃。程序員
你在實際工做中可能會很難有機會真正用到Java自帶的序列化技術了,工業界通常也會選擇一些更安全的對象編解碼方案例如Google的Protobuf等。因此,對於Java序列化,咱們沒必要再投入過多的精力學習,你花20分鐘讀完本文所掌握的知識,對於應付平常源碼閱讀中遇到的遺留的Java序列化技術應該是足夠了。算法
1、序列化是什麼
序列化機制容許將實現序列化的Java對象轉換成字節序列,這些字節序列能夠保存在磁盤上,或經過網絡傳輸,以備之後從新恢復成原來的對象。序列化機制使得對象能夠脫離程序的運行而獨立存在。編程
本文中用序列化來簡稱整個序列化和反序列化機制。 瀏覽器
2、爲何須要序列化
全部可能在網絡上傳輸的對象的類都應該是可序列化的,不然程序將會出現異常,好比RMI(Remote Method Invoke,即遠程方法調用,是JavaEE的基礎)過程當中的參數和返回值;全部須要保存到磁盤裏的對象的類都必須可序列化,好比Web應用中須要保存到HttpSession或ServletContext屬性的Java對象。安全
由於序列化是RMI過程的參數和返回值都必須實現的機制,而RMI又是Java EE技術的基礎——全部的分佈式應用經常須要跨平臺、跨網絡,因此要求全部傳遞的參數、返回值必須實現序列化。所以序列化機制是Java EE平臺的基礎。一般建議:程序建立的每一個JavaBean類都實現Serializable。服務器
3、序列化怎麼用
若是一個類的對象須要序列化,那麼在Java語法層面,這個類須要:網絡
下面咱們經過代碼示例來看看序列化最基本的用法。咱們建立了Person類,其擁有兩個基本類型的屬性,並實現了Serializable接口。testSerialize方法用來測試序列化,testDeserialize方法用來測試反序列化。框架
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Person one = new Person(12, 148.2); 10 Person two = new Person(35, 177.8); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Person.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Person.txt"))) { 26 Person one = (Person) input.readObject(); 27 Person two = (Person) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person implements Serializable { 40 int age; 41 double height; 42 43 public Person(int age, double height) { 44 this.age = age; 45 this.height = height; 46 } 47 48 @Override 49 public String toString() { 50 return "Person{" + 51 "age=" + age + 52 ", height=" + height + 53 '}'; 54 } 55 }
4、序列化深度探祕
若是某個類須要支持序列化功能,那麼它必須實現Serializable接口,不然會報 java.io.NotSerializableException。Serializable接口是一個標誌性接口(Marker Interface),也就是說,該接口並不包含任何具體的方法,是一個空接口,僅僅用來判斷該類是否可以序列化。JDK8中Serializable接口的源碼以下:dom
1 package java.io; 2 3 public interface Serializable { 4 }
在 ObjectOutputStream.java 的 writeObject0 方法中,咱們確實能夠看到對對象是否實現了 Serializable接口進行了驗證(第15行),不然會拋出 NotSerializableException 異常(第22行)。
1 private void writeObject0(Object obj, boolean unshared) 2 throws IOException 3 { 4 boolean oldMode = bout.setBlockDataMode(false); 5 depth++; 6 try { 7 ... 8 // remaining cases 9 if (obj instanceof String) { 10 writeString((String) obj, unshared); 11 } else if (cl.isArray()) { 12 writeArray(obj, desc, unshared); 13 } else if (obj instanceof Enum) { 14 writeEnum((Enum<?>) obj, desc, unshared); 15 } else if (obj instanceof Serializable) { 16 writeOrdinaryObject(obj, desc, unshared); 17 } else { 18 if (extendedDebugInfo) { 19 throw new NotSerializableException( 20 cl.getName() + "\n" + debugInfoStack.toString()); 21 } else { 22 throw new NotSerializableException(cl.getName()); 23 } 24 } 25 } finally { 26 depth--; 27 bout.setBlockDataMode(oldMode); 28 } 29 }
在第三部分「序列化怎麼用」部分的示例中,Person類的字段全都是基本類型,咱們知道基本類型其地址中直接存放的就是它的值,那若是是引用類型呢?引用類型其地址中存放的是指向堆內存中的一個地址,難道序列化時就是將這個地址進行了保存嗎?顯然,這是說不通的,由於對象的內存地址是可變的,在同一系統的不一樣運行時刻或者是不一樣系統中,對象的地址確定是不一樣的,所以,序列化內存地址沒有意義。
若是被序列化對象的字段是引用,那麼要求該引用的類型也是可序列化實現了Serializable接口的,不然沒法序列化。當對某個對象進行序列化時,系統會自動把該對象的全部Field依次進行序列化,若是某個Field引用到另外一個對象,則被引用的對象也會被序列化;若是被引用的對象的Field也引用了其餘對象,則被引用的對象也會被序列化,這種狀況被稱爲遞歸序列化。
若是對象A和對象B同時引用了對象C,那麼,當序列化對象A和對象B時,對象C會被序列化兩次嗎?答案顯然是不會。
要解釋這個問題,就不得不說一下Java序列化的基本算法了:
在一些特殊的場景下,若是一個類裏包含的某些Field值是敏感信息,例如銀行帳戶信息等,這時不但願系統將該Field值進行序列化;或者某個Field的類型是不可序列化的,所以不但願對該Field進行遞歸序列化,以免引起java.io.NotSerializableException異常。
此時,咱們就須要自定義序列化了。自定義序列化的經常使用方式有兩種:
咱們先看第一種方式,使用transient關鍵字。transient關鍵字只能用於修飾Field,不可修飾Java程序中的其餘成分。使用transient修飾的屬性,java序列化時,會忽略掉此字段,因此反序列化出的對象,被transient修飾的屬性是默認值。對於引用類型,值是null;基本類型,值是0;boolean類型,值是false。
下列代碼中,咱們把People的height字段設置爲transient,在反序列化時,可觀察到輸出爲默認值0.0。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Person one = new Person(12, 156.6); 10 Person two = new Person(16, 177.7); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Person.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Person.txt"))) { 26 Person one = (Person) input.readObject(); 27 Person two = (Person) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person implements Serializable{ 40 protected int age; 41 protected transient double height; 42 43 public Person() { 44 } 45 46 public Person(int age, double height) { 47 this.age = age; 48 this.height = height; 49 } 50 51 @Override 52 public String toString() { 53 return "Person{" + 54 "age=" + age + 55 ", height=" + height + 56 '}'; 57 } 58 }
程序輸出:
Person{age=12, height=0.0} Person{age=16, height=0.0} Process finished with exit code 0
使用transient關鍵字修飾Field雖然簡單、方便,但被transient修飾的Field將被徹底隔離在序列化機制以外,這樣致使在反序列化恢復Java對象時沒法取得該Field值。Java還提供了一種自定義序列化機制,經過這種自定義序列化機制可讓程序控制如何序列化各Field,甚至徹底不序列化某些Field(與使用transient關鍵字的效果相同)。在序列化和反序列化過程當中須要特殊處理的類應該提供以下特殊簽名的方法,這些特殊的方法用以實現自定義序列化。
private void writeObject(java.io.ObjectOutputStream out) throws IOException private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException; private void readObjectNoData() throws ObjectStreamException;
下面的示例代碼中,咱們在writeObject方法中對Person的字段進行了簡單的加密處理,在readObject方法中對其進行了相應的解密。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Person one = new Person(12, 156.6); 10 Person two = new Person(16, 177.7); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Person.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Person.txt"))) { 26 Person one = (Person) input.readObject(); 27 Person two = (Person) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person implements Serializable{ 40 protected int age; 41 protected double height; 42 43 public Person() { 44 } 45 46 public Person(int age, double height) { 47 this.age = age; 48 this.height = height; 49 } 50 51 private void writeObject(java.io.ObjectOutputStream out) 52 throws IOException { 53 System.out.println("Encryption!"); 54 out.writeInt(age + 1); 55 out.writeDouble(height - 1); 56 } 57 private void readObject(java.io.ObjectInputStream in) 58 throws IOException, ClassNotFoundException { 59 System.out.println("Decryption!"); 60 this.age = in.readInt() - 1; 61 this.height = in.readDouble() + 1; 62 } 63 64 @Override 65 public String toString() { 66 return "Person{" + 67 "age=" + age + 68 ", height=" + height + 69 '}'; 70 } 71 }
被序列化對象具備繼承關係時無非就兩種狀況,第一,該類具備子類,第二,該類具備父類。
當該類實現了Serializable接口且具備子類時,根據官方文檔中的說明,其子類自然具備可被序列化的屬性,不須要顯式實現Serializable接口;。
All subtypes of a serializable class are themselves serializable.
當該類實現了Serializable接口且具備父類時,,該類的父類須要實現Serializable接口嗎?在JDK8中Serializable接口的官方文檔中有這樣一段話:
1 /** 2 * ...... 3 * 4 * To allow subtypes of non-serializable classes to be serialized, the 5 * subtype may assume responsibility for saving and restoring the 6 * state of the supertype's public, protected, and (if accessible) 7 * package fields. The subtype may assume this responsibility only if 8 * the class it extends has an accessible no-arg constructor to 9 * initialize the class's state. It is an error to declare a class 10 * Serializable if this is not the case. The error will be detected at 11 * runtime. 12 * 13 * During deserialization, the fields of non-serializable classes will 14 * be initialized using the public or protected no-arg constructor of 15 * the class. A no-arg constructor must be accessible to the subclass 16 * that is serializable. The fields of serializable subclasses will 17 * be restored from the stream. 18 */
閱讀文檔咱們得知,爲了使得不可序列化類的子類可以序列化,其子類必須擔負起保存和恢復其超類的public、protected 和 package(if accessible)實例域的責任,且要求其父類必須有一個可訪問的無參構造函數以使得在反序列化時可以初始化實例域。
咱們寫代碼驗證一下,若是父類中沒有可訪問的無參構造函數會發生什麼,注意Person類中沒有無參構造函數。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Student one = new Student(12, 156.6, "1234"); 10 Student two = new Student(16, 177.7, "5678"); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Student.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Student.txt"))) { 26 Student one = (Student) input.readObject(); 27 Student two = (Student) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person{ 40 protected int age; 41 protected double height; 42 43 public Person(int age, double height) { 44 this.age = age; 45 this.height = height; 46 } 47 48 @Override 49 public String toString() { 50 return "Person{" + 51 "age=" + age + 52 ", height=" + height + 53 '}'; 54 } 55 } 56 57 class Student extends Person implements Serializable{ 58 private String id; 59 60 public Student(int age, double height, String id) { 61 super(age, height); 62 this.id = id; 63 } 64 65 @Override 66 public String toString() { 67 return "Student{" + 68 "age=" + age + 69 ", height=" + height + 70 ", id='" + id + '\'' + 71 '}'; 72 } 73 }
程序輸出產生異常:
java.io.InvalidClassException: Student; no valid constructor at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150) at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:768) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1775) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371) at SerializableTest.testDeserialize(SerializableTest.java:26) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) ... Process finished with exit code 0
當咱們爲Person類添加默認構造函數時:
1 class Person{ 2 protected int age; 3 protected double height; 4 5 public Person() { 6 } 7 8 public Person(int age, double height) { 9 this.age = age; 10 this.height = height; 11 } 12 13 @Override 14 public String toString() { 15 return "Person{" + 16 "age=" + age + 17 ", height=" + height + 18 '}'; 19 } 20 }
程序輸出以下,咱們可觀察到,父類中的字段都是默認值,只有子類中的字段獲得了正確的序列化。出現這種狀況的緣由是子類並無擔負起序列化父類中字段的責任。
Student{age=0, height=0.0, id='1234'} Student{age=0, height=0.0, id='5678'} Process finished with exit code 0
爲了解決上述問題,咱們須要藉助上一節中學到的知識,使用自定義的序列化方法writeObject和readObject來主動將父類中的字段進行序列化。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Student one = new Student(12, 156.6, "1234"); 10 Student two = new Student(16, 177.7, "5678"); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Studnet.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Studnet.txt"))) { 26 Student one = (Student) input.readObject(); 27 Student two = (Student) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person{ 40 protected int age; 41 protected double height; 42 43 public Person() { 44 } 45 46 public Person(int age, double height) { 47 this.age = age; 48 this.height = height; 49 } 50 51 @Override 52 public String toString() { 53 return "Person{" + 54 "age=" + age + 55 ", height=" + height + 56 '}'; 57 } 58 } 59 60 class Student extends Person implements Serializable{ 61 private String id; 62 63 public Student(int age, double height, String id) { 64 super(age, height); 65 this.id = id; 66 } 67 68 private void writeObject(java.io.ObjectOutputStream out) 69 throws IOException { 70 out.defaultWriteObject(); 71 out.writeInt(age); 72 out.writeDouble(height); 73 } 74 75 private void readObject(java.io.ObjectInputStream in) 76 throws IOException, ClassNotFoundException { 77 in.defaultReadObject(); 78 this.age = in.readInt(); 79 this.height = in.readDouble(); 80 } 81 82 @Override 83 public String toString() { 84 return "Student{" + 85 "age=" + age + 86 ", height=" + height + 87 ", id='" + id + '\'' + 88 '}'; 89 } 90 }
程序輸出以下,能夠看到徹底正確。
Student{age=12, height=156.6, id='1234'} Student{age=16, height=177.7, id='5678'} Process finished with exit code 0
5、serialVersionUID的做用及自動生成
咱們知道,反序列化必須擁有class文件,但隨着項目的升級,class文件也會升級,序列化怎麼保證升級先後的兼容性呢?
java序列化提供了一個private static final long serialVersionUID 的序列化版本號,只有版本號相同,即便更改了序列化屬性,對象也能夠正確被反序列化回來。若是反序列化使用的class的版本號與序列化時使用的不一致,反序列化會報InvalidClassException異常。下面是JDK 8中ArrayList的源碼中的serialVersionUID。
1 public class ArrayList<E> extends AbstractList<E> 2 implements List<E>, RandomAccess, Cloneable, java.io.Serializable 3 { 4 private static final long serialVersionUID = 8683452581122892189L; 5 6 /** 7 * Default initial capacity. 8 */ 9 private static final int DEFAULT_CAPACITY = 10; 10 ... 11 }
序列化版本號可自由指定,若是不指定,JVM會根據類信息本身計算一個版本號,這樣隨着class的升級,就沒法正確反序列化;不指定版本號另外一個明顯隱患是,不利於jvm間的移植,可能class文件沒有更改,但不一樣jvm可能計算的規則不同,這樣也會致使沒法反序列化。
什麼狀況下須要修改serialVersionUID呢?分三種狀況。
咱們在平常編程實踐中,通常會選擇使用IDE來自動生成serialVersionUID,這樣能夠最大化地減小重複的可能性。對於IntelliJ IDEA,自動生成serialVersionUID有三步:
6、序列化的缺點
Java序列化存在四個致命缺點,致使其不適用於網絡傳輸:
在真正的生產環境中,通常會選擇其它編解碼框架,領先的跨平臺結構化數據表示是 JSON 和 Protocol Buffers,也稱爲 protobuf。JSON 由 Douglas Crockford 設計用於瀏覽器與服務器通訊,Protocol Buffers 由谷歌設計用於在其服務器之間存儲和交換結構化數據。JSON 和 protobuf 之間最顯著的區別是 JSON 是基於文本的,而且是人類可讀的,而 protobuf 是二進制的,但效率更高。
7、參考文獻