Java中的序列化問題及解決

寫作背景
最近同事在一個VO對象中增加了一個字段,然後老大說需要評估一下是否有序列化問題,我們是將對象序列化到redis緩存中,所以查看其是否會有序列化問題,只要知道這個是對象會不會存儲在redis中,並且在redis中取出數據的時候需要反序列化。

一、序列化、反序列化
對象序列化:在Java中我們經常希望將對象存儲到緩存中,或者將其寫入硬盤資源中。其實這個過程就是將對象進行流化,不管是放入緩存中還是硬盤中,中間的傳輸過程都是以流的形式傳輸的。

反序列化:通過上面的對序列化的解釋,我們可以輕易的推測出反序列化就是將流對象轉化成對象的過程。

二、實現方式
1.實現Serializable接口(下面的論證都是基於這種方式)

2.實現Parcelable接口,是Android特有的序列化方式,可以藉助工具Android Parcelable Code Generator

三、序列化字段
1.實現java.io.Serializable接口代表這個對象中的所有字段都可以序列化嗎?

不是,實現java.io.Serializable接口只能說明這個對象可以別序列化,並不是代表所有字段都可以序列化。

2.如果想要序列化的對象沒有實現java.io.Serializable接口會報什麼錯誤?

將得到一個 RuntimeException 異常:主線程中出現異常 java.io.NotSerializableException。

3.如何將對象中的一個字段不進行序列化?

在這個字段上加transient關鍵字

4.對象中的靜態變量可以進行序列化嗎?

靜態變量不是對象狀態的一部分,因此它不參與序列化。所以將靜態變量聲明爲transient變量是沒有用處的。

5.對象的final變量可以設置爲不能序列化嗎?

不能,final變量將直接通過值參與序列化,所以將final變量聲明爲transient變量不會產生任何影響。

四、serialVersionUID
SerialVersionUID是一個標識符,當它通常使用對象的哈希碼序列化時會標記在對象上。這個標識符可以加可以不加,但是會有不同的影響。

如果你不添加serialVersionUID,也不會影響使用,但是存在即合理,既然存在這個字段,那麼他一定是有用的,當你序列化的時候這個UID會被寫入文件,當反序列話的時候會去讀取這個ID,並與反序列化的類中的UID對比,如果相同,那麼反序列化就成功,如果不同,反序列化就會失敗

當你不指定UID的時候,系統會根據類的結構生成相應的hash值賦值給UID,但是當你的類的結構發生變化,比如增加一個字段或者減少一個字段的時候,UID就會發生變化,那麼反序列話的時候兩個類的UID就不一樣了,就會反序列化失敗

所以手動指定UID,主要就是在類結構發生變化時,減少反序列化失敗的機率(如果類發生了非常規的結構變化,比如類名變化,成員變量的類型變化,就算是指定了UID,反序列化也會失敗)

使用IntelliJ IDEA自動生成serialVersionUID,這個功能IDEA是默認關閉的,開啓需要進行以下步驟:

1.settings–>Editor–>Inspections,然後在右側輸入UID進行搜索

2.勾選Serializable class without 'serialVersionUID'後面的複選框,右側Severity默認Warning即可

五、代碼準備(基於redis緩存)
1.MytestVo:添加serialVersionUID

@Data
public class MytestVo implements Serializable {
 
    private static final long serialVersionUID = 1587422156784638657L;
//    private static final long serialVersionUID = 158742215678463865L;
 
    private String id;
    private String name;
    private Integer age;
}
2.MytestWithOutUID:不添加serialVersionUID

@Data
public class MytestWithOutUID implements Serializable {
    private String id;
    private String name;
    private Integer age;
 
}
3.MytsetClient

@Slf4j
public class MytsetClient extends BaseControllerUT {
    @Autowired
    private CustomizationCacheService cacheService;
 
    @Test
    public void test(){
        MytestVo vo = new MytestVo();
        vo.setId("123");
        vo.setName("huchengong");
        vo.setAge(18);
        log.info("MytestVo:{}",vo);
        cacheService.setObjectAsynEx("ccccccccccccc",30*60,vo);
    }
 
    @Test
    public void test1(){
        try {
            MytestVo mytestVo = (MytestVo) cacheService.getObject("ccccccccccccc");
            log.info("MytestVo:{}",mytestVo);
        }catch (Exception e){
            log.error("MytestVo is error");
        }
 
    }
 
    @Test
    public void test2(){
        MytestWithOutUID mytestWithOutUID = new MytestWithOutUID();
        mytestWithOutUID.setId("123");
        mytestWithOutUID.setName("huchengong");
        mytestWithOutUID.setAge(18);
        log.info("MytestVo:{}",mytestWithOutUID);
        cacheService.setObjectAsynEx("dddddddddd",30*60,mytestWithOutUID);
    }
 
    @Test
    public void test3(){
        try {
            MytestWithOutUID mytestWithOutUID = (MytestWithOutUID) cacheService.getObject("dddddddddd");
            log.info("MytestWithOutUID:{}",mytestWithOutUID);
        }catch (Exception e){
            log.error("MytestWithOutUID is error");
        }
    }
}
4.緩存工具類自己編寫

六、驗證
1.添加serialVersionUID

1.1.保存緩存值

1.2.增加字段——無影響

1.3.減少字段——無影響

1.4.改變字段類型(該字段有值)——異常

 1.5.改變字段類型(該字段無值)——無影響

1.6.改變類名——異常

1.7. 修改serialVersionUID——異常

2.不添加serialVersionUID

2.1.保存緩存值

 2.2.增加字段——異常

2.3.減少字段——異常

七、解決方案
通過上面的驗證我們基本可以知道哪些情況下可能會發生異常,正常情況下我們建議使用serialVersionUID,因爲我們日常的修改中涉及比較多是增加字段(減少字段的很少),使用了serialVersionUID,增加字段就不會拋出異常。正常的解決方案是通過catch來截獲異常,因爲異常返回值必定爲null,需要查詢數據庫,然後更新緩存中的值,這個時候我們的serialVersionUID也就更新了,下次就不會出現異常了,代碼如下:

 public void test1(){
        // TODO: 2019/5/24 前面的業務邏輯 
        MytestVo mytestVo = null;
        try {
            mytestVo = (MytestVo) cacheService.getObject("gggggggggg");
            log.info("MytestVo:{}",mytestVo);
        }catch (Exception e){
            log.error("MytestVo is error");
        }
        
        if (null==mytestVo){
            // TODO: 2019/5/24 正常的業務應該是查詢數據庫 
            mytestVo = new MytestVo();
            mytestVo.setId("456");
            mytestVo.setName("libai");
            mytestVo.setAge(60);
            cacheService.setObjectAsynEx("gggggggggg",30*60,mytestVo);
        }
 
        // TODO: 2019/5/24 後面的業務邏輯 
 
    }
上面的方式可以避免反序列化異常情況的發生,不管serialVersionUID有沒有固定都是可以解決的,但是如果我們添加了serialVersionUID,只是涉及字段的增加或者減少,我們的代碼就可以簡化成下面這樣:

public void test1(){
        // TODO: 2019/5/24 前面的業務邏輯 
        MytestVo mytestVo = (MytestVo) cacheService.getObject("gggggggggg");
        log.info("MytestVo:{}", mytestVo);
        
        if (null==mytestVo){
            // TODO: 2019/5/24 正常的業務應該是查詢數據庫 
            mytestVo = new MytestVo();
            mytestVo.setId("456");
            mytestVo.setName("libai");
            mytestVo.setAge(60);
            cacheService.setObjectAsynEx("gggggggggg",30*60,mytestVo);
        }
 
        // TODO: 2019/5/24 後面的業務邏輯 
 
    }
參考文檔:https://www.jianshu.com/p/0034244a7115

https://baijiahao.baidu.com/s?id=1622011683975285944&wfr=spider&for=pc
————————————————
版權聲明:本文爲CSDN博主「hy_coming」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/hy_coming/article/details/90301468

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章