序列化和反序列化的概念
- 序列化:把java對象轉換為字節序列的過程稱為對象的序列化,這些字節序列可以被保存在磁盤上或通過網絡傳輸,以備以后重新恢復原來的對象
- 反序列化:把字節序列恢復為對象的過程稱為對象的反序列化。序列化機制使得對象可以脫離程序的運行而獨立存在
序列化的功能/用途
- 持久化對象:Java平臺允許我們在內存中創建可復用的Java對象,但一般情況下,只有當JVM處于運行時,這些對象才可能存在,即,
這些對象的生命周期不會比JVM的生命周期更長。但在現實應用中,就可能要求在JVM停止運行之后能夠保存(持久化)指定的對象,并在將來重新讀取被保存的對象。把對象的字節序列永久地保存到硬盤上,通常存放在一個文件中,以此實現該功能 。java中的對象的內部狀態只保存在內存中,其生命周期最長與JVM的生命周期一樣,即JVM停止之后,所有對象都會被銷毀。 - 網絡傳輸:在網絡上傳送對象的字節序列。
實際應用
- 在很多應用中,需要對某些對象進行序列化,讓它們離開內存空間,入住物理硬盤,以便長期保存。比如最常見的是Web服務器中的Session對象,當有 10萬用戶并發訪問,就有可能出現10萬個Session對象,內存可能吃不消,于是Web容器就會把一些seesion先序列化到硬盤中,等要用了,再把保存在硬盤中的對象還原到內存中。
- 當兩個進程在進行遠程通信時,彼此可以發送各種類型的數據。無論是何種類型的數據,都會以二進制序列的形式在網絡上傳送。發送方需要把這個Java對象轉換為字節序列,才能在網絡上傳送;接收方則需要把字節序列再恢復為Java對象。
實現
- java.io.Serializable接口,那么它就可以被序列化
- Externalizable:
Serializable接口
· 優點:內建支持
· 優點:易于實現
· 缺點:占用空間過大
· 缺點:由于額外的開銷導致速度變比較慢
Externalizable接口
· 優點:開銷較少(程序員決定存儲什么)
· 優點:可能的速度提升
· 缺點:虛擬機不提供任何幫助,也就是說所有的工作都落到了開發人員的肩上。
在兩者之間如何選擇要根據應用程序的需求來定。Serializable通常是最簡單的解決方案,但是它可能會導致出現不可接受的性能問題或空間問題;在出現這些問題的情況下,Externalizable可能是一條可行之路。
JDK類庫中的序列化API
- java.io.ObjectOutputStream代表對象輸出流,它的writeObject(Object obj)方法可對參數指定的obj對象進行序列化,把得到的字節序列寫到一個目標輸出流中。
- java.io.ObjectInputStream代表對象輸入流,它的readObject()方法從一個源輸入流中讀取字節序列,再把它們反序列化為一個對象,并將其返回。
序列化與反序列化的編程實現
實現序列化接口的類
public class Person implements Serializable{
private static final long serialVersionUID = -1015228989208411177L;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
序列化過程
public class WriteObject {
public static void main(String[] args) {
ObjectOutputStream oos = null;
try {
//1.創建一個ObjectOutputStream
oos = new ObjectOutputStream(new FileOutputStream("/home/sunyan/object.txt"));
Person per = new Person("孫悟空", 500);
//2.將per對象寫入輸入流
oos.writeObject(per);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
if(oos != null){
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
反序列化
public class ReadObject {
public static void main(String[] args) {
ObjectInputStream ois = null;
try {
//1.創建一個ObjectInputStream輸入流
ois = new ObjectInputStream(new FileInputStream("/home/sunyan/object.txt"));
//2.從輸入流中讀取一個Java對象,并將其強制類型轉換為Person對象
Person p = (Person) ois.readObject();
System.out.println("名字為:" + p1.getName() + "\n年齡為:" + p1.getAge());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally{
try {
if (ois == null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
執行結果:
- 如果我們向文件中使用序列化機制寫入了多個Java對象,使用反序列化機制恢復對象必須按照實際寫入的順序讀取。
序列化
Person per1 = new Person("孫悟空", 500);
Person per2 = new Person("孫小妹", 50);
oos.writeObject(per1);
oos.writeObject(per2);
反序列化
Person p1 = (Person) ois.readObject();
Person p2 = (Person) ois.readObject();
System.out.println("名字為:" + p1.getName() + "\n年齡為:" + p1.getAge());
System.out.println("名字為:" + p2.getName() + "\n年齡為:" + p2.getAge());
執行結果
- 對象引用的序列化
如果類的屬性不是基本類型或者String類型,而是另一個引用類型,那么這個引用類型必須是可序列化的,否則該類也是不可序列化的,即使該類實現了Serializable,Externalizable接口。
public class Teacher implements Serializable{
private String name;
//類的屬性是引用類型,也必須序列化。
//如果Person是不可序列化的,無論Teacher實現Serializable,Externalizable接口,則Teacher
//都是不可序列化的。
private Person student;
public Teacher(String name, Person student) {
super();
this.name = name;
this.student = student;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person getStudent() {
return student;
}
public void setStudent(Person student) {
this.student = student;
}
}
上述代碼中,Teacher中有一個引用類型student,若Person未實現接口Serializable。即
public class Person implements Serializable{
}
則在序列化過程中,會報錯
- Transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字,可以阻止該變量被序列化到文件中,在被反序列化后,transient 變量的值被設為初始值,如 int 型的是 0,對象型的是 null。
在Person中更改如下代碼
private transient int age;
此時1中的代碼,經序列化和反序列化后,執行結果如下
-
s?e?r?i?a?l?V?e?r?s?i?o?n?U?I?D
s?e?r?i?a?l?V?e?r?s?i?o?n?U?I?D?:? ?字?面?意?思?上?是?序?列?化?的?版?本?號?,凡是實現Serializable接口的類都有一個表示序列化版本標識符的靜態變量。序列化 ID 是否一致,決定 虛擬機是否允許反序列化。
實現Serializable接口的類如果類中沒有添加serialVersionUID,那么就會出現如下的警告提示
serialVersionUID有兩種生成方式:
采用 Add default serial version ID
這種方式生成的serialVersionUID是1L,例如:
private static final long serialVersionUID = 1L;
采用 Add generated serial version ID這種方式生成的serialVersionUID是根據類名,接口名,方法和屬性等來生成的,例如:
private static final long serialVersionUID = -1015228989208411177L;
對1中的代碼序列化后,修改
private static final long serialVersionUID = -1015228989208411177L;
為
private static final long serialVersionUID = -1015228989208411178L;
此時,進行反序列化,會報錯
反序列化漏洞危害
當應用代碼從用戶接受序列化數據,并試圖反序列化改數據進行下一步處理時,會產生反序列化漏洞,其中最有危害性的就是遠程代碼注入。
這種漏洞產生原因是,java類ObjectInputStream在執行反序列化時,并不會對自身的輸入進行檢查,這就說明惡意攻擊者可能也可以構建特定的輸入,在 ObjectInputStream類反序列化之后會產生非正常結果,利用這一方法就可以實現遠程執行任意代碼。
最后再加一些相關知識點
1、聲明為static和transient的成員數據不能被串行化,因為static代表類的狀態,transient代表對象的臨時數據。
2、要想將父類對象也序列化,就需要讓父類也實現Serializable 接口。
3、服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,比如密碼字符串等,希望對該密碼字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時,才可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數據安全。