反序列化引起的線上問題思考

背景:周一早上例行巡檢,發(fā)現(xiàn)有一個crash出現(xiàn)兩次,剛好新版本剛灰度10%比例。立即暫停灰度,確認(rèn)問題影響范圍。

一、問題表現(xiàn)

1. bugly上對應(yīng)crash異常上報如下:
企業(yè)微信截圖_36784770-628a-4de5-bdda-bc4392f0a97a.png
2.定位代碼位置如下
企業(yè)微信截圖_c720b4ef-aad0-4c47-a038-f72893451de8.png

代碼有部分直接馬賽克,非常簡單直接的代碼一個data對象,實現(xiàn)序列化接口,里面有定義序列號。成員變量set集合直接創(chuàng)建對象,并給默認(rèn)初始值。提供一個get方法返回這個set.

并沒有地方對這個對象賦值為null,照理說不應(yīng)該存在為空的場景。 如果是自己遇到這個問題也可以看看自己分析思路、方向。

3.問題原因

上面的data對象做了本地持久化存儲。set字段是新版本需求加的。歷史版本有緩存一個對象在本地。覆蓋升級安裝時,緩存數(shù)據(jù)反序列化,因為老版本沒有set字段,反序列化后 set會變成null.
這部分歷史代碼還沒有用KT,所以外面在調(diào)用時,如果沒有非空判斷。就存在crash問題。

實際開發(fā)時候,多人協(xié)作時這種歷史代碼不是自己開發(fā)不了解它的使用邏輯時,很容易出現(xiàn)這種問題。而且測試不容易發(fā)現(xiàn)。因為啟動時候會請求服務(wù)器數(shù)據(jù)更新緩存。出現(xiàn)crash用戶剛好是啟動階段請求服務(wù)器接口慢,在使用到數(shù)據(jù)反序列化時還沒有請求回來數(shù)據(jù)更新緩存出現(xiàn)。

我正常去某個對象加個字段,并且成員變量上來就new出來的對象怎么會空呢???
大部分使用場景都是直接new 對象使用,肯定不會出現(xiàn)空。但是如果有持久化存儲這種序列化對象,在反序列化時候上面的代碼就是可能為空,所以使用時注意判斷

4.問題復(fù)現(xiàn)

安裝老版本,抓包看到請求數(shù)據(jù)后,覆蓋安裝新版本,mock接口延遲返回。就能復(fù)現(xiàn),異常和bugly一致。

5.問題修復(fù)

問題修復(fù)就很簡單了,因為暫停了灰度,需要快速修復(fù)上線繼續(xù)灰度放量,直接獲取方法里面判斷如果為空 創(chuàng)建對象返回。
后續(xù)修復(fù)有幾個方向:1.反序列化時增加邏輯去兜底 開發(fā)者不用關(guān)注需要做這種兜底 2.增加一些探測機制

二、序列化反序列化知識點梳理

遇到這個問題時,除了去看歷史邏輯不熟悉耗費時間,發(fā)現(xiàn)序列化反序列化相關(guān)很多知識點已經(jīng)比較模糊了,整個問題確認(rèn)驗證比較占用時間。梳理下相關(guān)知識點鞏固下,也可以自己看下下面的問題是不是都非常清楚。

1.反序列化后的對象會重新調(diào)用構(gòu)造函數(shù)嗎?
答:不會, 因為是從二進(jìn)制直接解析出來的

2.序列化與反序列化后的對象是什么關(guān)系?
答:是一個深拷貝, 前后對象的引用地址不同

3.序列化ID是什么,有什么用?不用有什么影響?更改了有什么影響
serialVersionUID 是一個 private static final long 型 ID, 一般是對象的哈希碼,可以使用 serialver 這個 JDK 工具來查看序列化對象的序列化ID。
用于對象的版本控制。也可以在類文件中指定 serialVersionUID。不指定serialVersionUID時,當(dāng)你添加或修改類中的任何字段時,則已序列化類將無法恢復(fù),因為為新類和舊序列化對象生成的 serialVersionUID 將有所不同。Java 序列化過程依賴于正確的序列化對象恢復(fù)狀態(tài)的,并在序列化對象序列版本不匹配的情況下引發(fā)InvalidclassException

4.針對反序列化場景新增加字段有沒有方法可以規(guī)避出現(xiàn)空指針場景?
從上面的描述我們知道,如果緩存了一份老版本的數(shù)據(jù)在本地,新版本修改了字段,直接從老版本緩存反序列化數(shù)據(jù)使用,可能出現(xiàn)為空場景。大項目協(xié)作時,從架構(gòu)上避免出現(xiàn)上述問題??梢圆捎靡恍┭a全的實現(xiàn),比如:JSON。代碼如下:

public class SPUtilFile {
    private static final String PREF_NAME = "my_preferences";

    public static void saveObject(Context context, String key, Serializable object) {
        SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(object);
            objectOutputStream.close();
            String base64String = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.DEFAULT);
            editor.putString(key, base64String);
            editor.apply();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static <T extends Serializable> T getObject(Context context, String key, Class<T> clazz) {
        SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
        String base64String = sharedPreferences.getString(key, null);
        if (base64String == null) {
            return null;
        }
        byte[] byteArray = Base64.decode(base64String, Base64.DEFAULT);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
        try {
            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
            T object = (T) objectInputStream.readObject();
            objectInputStream.close();
            return object;
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            Log.d("MainActivity", "異常");
        }
        return null;
    }

執(zhí)行一樣的操作,先緩存一份到本地,然后下次反序列數(shù)據(jù),新增加的字段比如 HashSet<String> set = new HashSet(); 的set就會出現(xiàn)空,使用時候沒注意就空指針了。 但是如果使用下面的方法,使用JSON轉(zhuǎn)換,它會自動補全set反序列化出來是一個空數(shù)組,不是null。

public class SPUtil {
    private static final String PREF_NAME = "my_preferences";

    public static void saveObject(Context context, String key, Serializable object) {
        SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        String jsonString = JSON.toJSONString(object);
        editor.putString(key, jsonString);
        editor.apply();
    }

    public static <T extends Serializable> T getObject(Context context, String key, Class<T> clazz) {
        SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
        String jsonString = sharedPreferences.getString(key, null);
        if (jsonString == null) {
            return null;
        }
        return JSON.parseObject(jsonString, clazz);
    }
}

5.序列化時如果某個成員變量不序列化怎么處理?反序列化后是什么樣的?
如果你不希望任何字段是對象的狀態(tài)的一部分,然后聲明它靜態(tài)或瞬態(tài)根據(jù)你的需要,這樣就不會是在 Java 序列化過程中被包含
反序列化后對應(yīng)字段是null,因為序列化時候根本沒存進(jìn)去

6.某個對象中有一個內(nèi)部對象未實現(xiàn)序列化,序列化時候會出現(xiàn)什么
運行時將引發(fā)不可序列化異常 NotSerializableException

7.如果當(dāng)前類是可序列化,但父類不是,父類的成員反序列化后如何
反序列化后為空

8.是否可以自定義序列化?怎么做
對于序列化一個對象需調(diào)用 ObjectOutputStream. writeObject(saveThisObject),并用ObjectInputStream. readObject()讀取對象,但 Java虛擬機為你提供的還有一件事,是定義這兩個方法。如果在類中定義這兩種方法,則JVM將調(diào)用這兩種方法,而不是應(yīng)用默認(rèn)序列化機制。
你可以在此處通過執(zhí)行任何類型的預(yù)處理或后處理任務(wù)來自定義對象序列化和反序列化的行

9.序列化源碼看過嗎?主要用到哪些,枚舉特殊嗎?
readOb jectO 的用法、vriteObject O、readExternal () 和 writeExternal0?Java 序列化由java.io. ObjectOutputStream類完成。該類是一個篩選器流,它封裝在較低級別的字節(jié)流中,以處理序列化機制。要通過序列化機制存儲任何對象,我們調(diào)用
ObjectOutputStream. writeObject(savethisobject),并反序列化該對象,我們稱之為ObjectInputStream.readObjectO方法。調(diào)用以 writeObject( 方法在 java 中觸發(fā)序列化過程。
關(guān)于 readobject()方法,需要注意的一點很重要一點是,它用于從持久性讀取字節(jié),并從這些字節(jié)創(chuàng)建對象,并返回一個對象,該對象需要類型強制轉(zhuǎn)換為正確的類型

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容