概念
序列化:就是把對(duì)象轉(zhuǎn)化成字節(jié)步咪。
反序列化:把字節(jié)數(shù)據(jù)轉(zhuǎn)換成對(duì)象论皆。
對(duì)象序列化場(chǎng)景:
1、對(duì)象網(wǎng)絡(luò)傳輸
例如:在微服務(wù)系統(tǒng)中或給第三方提供接口調(diào)用時(shí)猾漫,使用rpc進(jìn)行調(diào)用点晴,一般會(huì)把對(duì)象轉(zhuǎn)化成字節(jié)序列,才能在網(wǎng)絡(luò)上傳輸悯周;接收方則需要把字節(jié)序列再轉(zhuǎn)化為java對(duì)象粒督。
2、對(duì)象保存至文件中
例如:hibernate中的二級(jí)緩存:把從數(shù)據(jù)庫(kù)中查詢出的對(duì)象禽翼,序列化轉(zhuǎn)存到硬盤中屠橄,下次讀取的時(shí)候,首先從內(nèi)存中找是否有該對(duì)象闰挡,如果沒有在去二級(jí)緩存(硬盤)中去查找锐墙。減少數(shù)據(jù)庫(kù)的查詢次數(shù),提升性能长酗。
3溪北、tomcat的鈍化和活化
tomcat 的session 鈍化和活化之 StandarManager :
當(dāng)Tomcat服務(wù)器關(guān)閉或者重啟時(shí)tomcat服務(wù)器會(huì)將當(dāng)前內(nèi)存中的session對(duì)象鈍化到服務(wù)器文件系統(tǒng)中;
另一種情況是web應(yīng)用程序被重新加載時(shí)(其實(shí)原理也是重啟tomcat),內(nèi)存中的session對(duì)象也會(huì)被鈍化到服務(wù)器的文件系統(tǒng)中
當(dāng)系統(tǒng)啟動(dòng)時(shí)之拨,會(huì)把序列化到硬盤上session重新加載到內(nèi)存中來茉继。這樣用戶還保持這登錄狀態(tài),提供系統(tǒng)的可用性蚀乔。
這樣馒疹,tomcat重啟,如果用戶在tomcat重啟之前登錄過乙墙,然后在tomcat重啟后可以不需要登錄(前提是session沒過期前,默認(rèn)是30分鐘過期)生均。tomcat 的session 鈍化和活化之 Persistentmanager:
當(dāng)網(wǎng)站有大量用戶訪問的時(shí)候听想,服務(wù)器會(huì)創(chuàng)建大量的session,會(huì)占用大量的服務(wù)器內(nèi)存資源马胧,當(dāng)用戶開著瀏覽器一分鐘不操作頁面的話建議將session鈍化汉买,將session生成文件放在tomcat工作目錄下。
1. java 序列化 Serializable
java 中只要對(duì)象實(shí)現(xiàn)了 java.io.Serializable 就可以進(jìn)行序列化佩脊。
public class User implements Serializable {
private String userName;
private String password;
private String addr;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getAddr() {
return addr;
}
public void setAddr(String addr) {
this.addr = addr;
}
@Override
public String toString() {
return "User [userName=" + userName + ", password=" + password + ", addr=" + addr + "]";
}
}
該 User 類實(shí)現(xiàn)了 Serializable 接口蛙粘,那么該類應(yīng)該怎么序列化和發(fā)序列化呢?
2. ObjectInputStream 和 ObjectOutputStream
Java IO 包中為我們提供了 ObjectInputStream 和 ObjectOutputStream 兩個(gè)類威彰。
java.io.ObjectOutputStream 類實(shí)現(xiàn)類的序列化功能出牧。
java.io.ObjectInputStream 類實(shí)現(xiàn)了反序列化功能。
示例如下:
public class Test{
public static void main(String[] args) throws Exception {
File file = new File("d:\\a.user");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
User user1 = new User();
user1.setUserName("zhangsan");
user1.setPassword("123456");
user1.setAddr("北京中關(guān)村");
oos.writeObject(user1);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
User user2 = (User)ois.readObject();
System.out.println(user2);
}
}
輸出結(jié)果:
User [userName=zhangsan, password=123456, addr=北京中關(guān)村]
- 使用 ObjectOutputStream 把 user1 實(shí)例序列化到 d:\user 文件中歇盼。
- 使用 ObjectInputStream 把 d:\user 文件中的數(shù)據(jù)反序列化成 user2 實(shí)舔痕,并打印。
如果考慮安全問題豹缀,我們不想把密碼序列化進(jìn)行保存伯复,那么該怎么做呢?
3. transient關(guān)鍵字
當(dāng)某個(gè)字段被聲明為transient后邢笙,默認(rèn)序列化機(jī)制就會(huì)忽略該字段啸如。此處將User類中的password字段聲明為transient,如下所示氮惯,
public class User implements Serializable {
private String userName;
private transient String password;
private String addr;
... ...
然后在執(zhí)行Test類的 main 方法叮雳,執(zhí)行結(jié)果如下:
輸出結(jié)果:
User [userName=zhangsan, password=null, addr=北京中關(guān)村]
當(dāng)我們把 User 對(duì)象序列化保存到文件中,這時(shí) User 類結(jié)構(gòu)添加了一個(gè)新字段妇汗,那么它能成功反序列化嗎债鸡?
serialVersionUID的作用
User 類中添加一個(gè)新屬性 email 字段,如下圖:執(zhí)行結(jié)果如下:
Exception in thread "main" java.io.InvalidClassException: cn.com.infcn.serial.User; local class incompatible: stream classdesc serialVersionUID = 1318824539146791009, local class serialVersionUID = 7884536922902331245
執(zhí)行反序列化報(bào) java.io.InvalidClassException 異常铛纬。這是由于 User 類修改了厌均,
也就是修改過后的class,不兼容了告唆,處于安全機(jī)制考慮棺弊,程序拋出了錯(cuò)誤晶密,并且拒絕載入。從異常信息中可以看出模她,它是根據(jù) serialVersionUID 值進(jìn)行判斷類是否修改過稻艰。
如果在添加新字段 email 后,還可以繼續(xù)加載之前的字段怎么辦呢侈净?
serialVersionUID 的值和報(bào)錯(cuò)中的 "stream classdesc serialVersionUID" 的值一樣就可以反序列化了。
如果類中沒有顯示的聲明 serialVersionUID 屬性畜侦,那么java編譯器會(huì)自動(dòng)為我們生成一個(gè) serialVersionUID (應(yīng)該是根據(jù) 屬性和方法進(jìn)行摘要算出來的元扔,方法里面內(nèi)容變動(dòng) serialVersionUID 的值不會(huì)改變。)
如果 User 對(duì)象升級(jí)版本旋膳,修改了結(jié)構(gòu)澎语,而且不想兼容之前的版本,那么只需要修改下 serialVersionUID 的值就可以了验懊。
建議擅羞,每個(gè)需要序列化的對(duì)象,都要添加一個(gè) serialVersionUID 字段义图。
如果需要把設(shè)置的 transient 的字段也需要序列化和發(fā)序列化减俏,我們應(yīng)該怎么辦?我們需要對(duì)密碼加密序列化碱工,反序列化后解密處理垄懂,又應(yīng)該怎么做?
readObject 和 writeObject
crypto 方法痛垛,實(shí)現(xiàn)加解密功能草慧。
/**
* 簡(jiǎn)單加密加密解密字符串 加密解密思路:先將字符串變成byte數(shù)組,再將數(shù)組每位與key做位運(yùn)算匙头,得到新的數(shù)組就是加密或解密后的byte數(shù)組.
* 知識(shí):^ 是java位運(yùn)算漫谷,可以百度了解下,a = b ^ skey 反之也成立蹂析,即b = a ^ skey
*
* @param str 解密/加密 字符串
* @return
* @throws Exception
*/
static String crypto(String str) {
try {
byte skey = (byte) 88; //密鑰
byte[] bytes = str.getBytes("GBK");
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) (bytes[i] ^ skey);
}
return new String(bytes, "GBK");
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
我們只需要在當(dāng)前 User 類中添加 readObject() 和 writeObject() 方法舔示,在 writeObject 方法中實(shí)現(xiàn)對(duì) password 的字段加密,在 readObject 方法中實(shí)現(xiàn)對(duì) password 字段解密电抚,并賦值給 User 對(duì)象即可惕稻。
readObject() 和 writeObject() 可以實(shí)現(xiàn)對(duì) transient 和 非transient字段進(jìn)行序列化。
ArrayList 序列化源碼分析
我們知道蝙叛,ArrayList 是通過數(shù)組進(jìn)行存儲(chǔ)數(shù)據(jù)的俺祠,當(dāng)數(shù)組中元素達(dá)到數(shù)組的最大容量時(shí),會(huì)自動(dòng)生成一個(gè)更大的數(shù)組,并復(fù)制到更大的數(shù)組中蜘渣。
打開ArrayList 源碼淌铐,我們可以知道,數(shù)據(jù)是存儲(chǔ)在 Object[] elementData 數(shù)組中蔫缸。
該屬性是 transient 關(guān)鍵字修飾的腿准,通過上面代碼可以知道,用 transient 關(guān)鍵字修飾的字段拾碌,默認(rèn)是不能被序列化的吐葱。ArrayList 如果要實(shí)現(xiàn)序列化,那么就必須通過 readObject() 和 writeObject() 方法去實(shí)現(xiàn)序列化校翔,那么他這是多此一舉嗎弟跑?
writeObject() 方法
通過源碼,我們可以看到展融,ArrayList 序列化數(shù)組元素時(shí)做了優(yōu)化。
因?yàn)?ArrayList 的 elementData 數(shù)組大小豫柬,不是ArrayList 的實(shí)際容量告希,這里只把實(shí)際存儲(chǔ)在 elementData中的數(shù)據(jù),進(jìn)行序列化烧给。這樣減少了序列化的流大小燕偶。