反序列化時(shí)InvalidClassException異常修復(fù)方案

問(wèn)題來(lái)源

最近一次項(xiàng)目的版本開(kāi)發(fā)中,產(chǎn)品經(jīng)理希望新增加一些場(chǎng)景功能,這時(shí)我們?cè)緮?shù)據(jù)結(jié)構(gòu)DeskPushDetailsBean的字段不夠用,只能新增加字段tempDescribe,開(kāi)發(fā)完成后調(diào)試功能滿足預(yù)期,ok,發(fā)版了灰度上線,一天后從友盟統(tǒng)計(jì)平臺(tái)看到某個(gè)用戶手機(jī)上5分鐘內(nèi)連續(xù)報(bào)錯(cuò)了將近8次(估計(jì)用戶得卸載我們app了),手機(jī)型號(hào):OPPO A83 (android 7.1.1),詳細(xì)報(bào)錯(cuò)為:

--logversion:utracea
Process Name: 'com.xxx.weather'
Thread Name: 'main'
Back traces starts.
java.lang.RuntimeException: Unable to start activity ComponentInfo{XXXBottomViewActivity}: java.lang.RuntimeException: Parcelable encountered IOException reading a Serializable object (name = com.xiaoniu.deskpushuikit.bean.DeskPushDetailsBean)
 at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2977)
 at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3101)
 at android.app.ActivityThread.-wrap12(ActivityThread.java)
 at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1737)
 at f.j.a.a.a.handleMessage(ActivityThreadCallback.java:4)
 at android.os.Handler.dispatchMessage(Handler.java:106)
 at android.os.Looper.loop(Looper.java:232)
 at android.app.ActivityThread.main(ActivityThread.java:6914)
 at java.lang.reflect.Method.invoke(Native Method)
 at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1103)
 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:964)
Caused by: java.lang.RuntimeException: Parcelable encountered IOException reading a Serializable object (name = com.xxxkit.bean.DeskPushDetailsBean)
 at android.os.Parcel.readSerializable(Parcel.java:2650)
 at android.os.Parcel.readValue(Parcel.java:2440)
 at android.os.Parcel.readArrayMapInternal(Parcel.java:2756)
 at android.os.BaseBundle.unparcel(BaseBundle.java:269)
 at android.os.BaseBundle.getBoolean(BaseBundle.java:731)
 at android.content.Intent.getBooleanExtra(Intent.java:6155)
 at com.jess.arms.integration.ActivityLifecycle.onActivityCreated(ActivityLifecycle.java:2)
 at android.app.Application.dispatchActivityCreated(Application.java:206)
 at android.app.Activity.onCreate(Activity.java:1041)
 at androidx.core.app.ComponentActivity.onCreate(ComponentActivity.java:1)
 at androidx.activity.ComponentActivity.onCreate(ComponentActivity.java:1)
 at androidx.fragment.app.FragmentActivity.onCreate(FragmentActivity.java:16)

 at com.xxx.XXXBottomViewActivity.onCreate(XXXBottomViewActivity.java:3)
 at android.app.Activity.performCreate(Activity.java:6987)
 at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118)
 at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2925)
 ... 10 more
Caused by: java.io.InvalidClassException: com.xiaoniu.deskpushuikit.bean.DeskPushDetailsBean; local class incompatible: stream classdesc serialVersionUID = 6163620063913001067, local class serialVersionUID = 8478373576351162083
 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:606)
 at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)
 at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
 at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1772)
 at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
 at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
 at android.os.Parcel.readSerializable(Parcel.java:2648)
 ... 26 more

其中重要的關(guān)鍵報(bào)錯(cuò)信息在第2個(gè)Caused by,即java.io.InvalidClassException: com.xiaoniu.deskpushuikit.bean.DeskPushDetailsBean; local class incompatible: stream classdesc serialVersionUID = 6163620063913001067, local class serialVersionUID = 8478373576351162083;

原因分析

打開(kāi)項(xiàng)目,查看DeskPushDetailsBean類;

public class DeskPushDetailsBean implements Serializable {
 public DayWeatherBean today;
 public DayWeatherBean tomorrow;
 public MinutelyBean minutely;
 public NewsBean news;
 public WarningBean alert;
 public RealWeatherBean realTime;
 public CustomTopBean customTopBean;
 public String pushType;
 public boolean showCloseButton;
 public String triggerTime;
 public TemputerTextBean tempDescribe;//新增屬性
}

發(fā)現(xiàn)它序列化時(shí)implements Serializable,點(diǎn)擊走進(jìn)Serializable源碼或者查看文檔能不能發(fā)現(xiàn)一些線索,我們可以看見(jiàn)下面一段注釋備注:

If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification. However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassExceptions during deserialization. Therefore, to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit serialVersionUID value. It is also strongly advised that explicit serialVersionUID declarations use the private modifier where possible, since such declarations apply only to the immediately declaring class--serialVersionUID fields are not useful as inherited members. Array classes cannot declare an explicit serialVersionUID, so they always have the default computed value, but the requirement for matching serialVersionUID values is waived for array classes. Android implementation of serialVersionUID computation will change slightly for some classes if you're targeting android N. In order to preserve compatibility, this change is only enabled is the application target SDK version is set to 24 or higher. It is highly recommended to use an explicit serialVersionUID field to avoid compatibility issues.

簡(jiǎn)單翻譯一下(英文很差、湊合著看):
如果一個(gè)可序列化的類沒(méi)有明確指定serialVersionUID,那么它將如同Java規(guī)范文檔中描述那樣,序列化運(yùn)行時(shí)將基于這個(gè)類的各個(gè)方面計(jì)算默認(rèn)的serialVersionUID值。然后,強(qiáng)烈推薦所有實(shí)現(xiàn)serializable的序列化類明確的指定serialVersionUID值,因?yàn)槟J(rèn)serialVersionUID值的計(jì)算對(duì)類的細(xì)節(jié)高度敏感,可能取決于編譯器的實(shí)現(xiàn)而改變,因此在反序列化過(guò)程中將導(dǎo)致出乎意料的InvalidClassException,因此,要保證不同java編譯器實(shí)現(xiàn)之間的serialVersionUID值一致,可序列化的類必須聲明一個(gè)明確的serialVersionUID值。強(qiáng)烈建議在可能的情況下使用private修飾符顯式的聲明serialVersionUID,因?yàn)檫@樣的聲明只適用于當(dāng)前直接聲明的類——serialVersionUID字段作為子類繼承的成員是沒(méi)有用的。數(shù)組類無(wú)法顯式的聲明serialVersionUID,因此它們始終具有默認(rèn)的計(jì)算值,但是數(shù)組類不需要匹配serialVersionUID值。

看到這里,問(wèn)題很明顯了,我們?cè)谠瓉?lái)類中增加了tempDescribe變量,但是一直并沒(méi)有明確指定serialVersionUID值,事實(shí)上,新的變量會(huì)導(dǎo)致編譯器運(yùn)行時(shí)會(huì)為它自動(dòng)生成新的serialVersionUID值,這個(gè)值和原來(lái)自動(dòng)生成的serialVersionUID值不一致;

生成serialVersionUID值

AndroidStudio可以很方便的增加serialVersionUID值,設(shè)置如下:

File->Settings->Editor->Inspections,輸入serializable搜索,找到Serializable class without ‘serialVersionUID’項(xiàng),勾選apply,ok即可;

image.png

然后找到需要生成serialVersionUID值的目標(biāo)類,雙擊類文件中類名,左側(cè)會(huì)出來(lái)一個(gè)小燈泡,點(diǎn)擊“Add ‘serialVersionUID’ field,編譯器就自動(dòng)幫我們生成好了serialVersionUID值;

image.png

生成完后的如下,細(xì)心的同學(xué)應(yīng)該已經(jīng)發(fā)現(xiàn)了,這里生成的值就是開(kāi)始報(bào)錯(cuò)異常InvalidClassException后面的local class serialVersionUID = 8478373576351162083:

image.png

需要注意的是:不管是增加、減少成員變量個(gè)數(shù)、或者改變成員變量的名字,都會(huì)導(dǎo)致自動(dòng)生成serialVersionUID值的改變;

解決方案

根據(jù)文檔,既然我們要保持serialVersionUID的值不變,那么就不能用最新的增加tempDescribe變量后的代碼生成serialVersionUID值,而是要用之前的代碼生成,通過(guò)git歷史記錄找到之前的舊代碼,再重新生成則得到 private static final long serialVersionUID = 6163620063913001067L;這個(gè)值也正好是開(kāi)始報(bào)錯(cuò)異常InvalidClassException里面的前部分local class incompatible: stream classdesc serialVersionUID = 6163620063913001067中的值,至此,我們只需要在最新代碼中增加下面一行即可

private static final long serialVersionUID = 6163620063913001067L;

其實(shí)查看《阿里巴巴Java開(kāi)發(fā)手冊(cè)終極版v1.3.0》文檔,里面 OOP 規(guī)約的第10條也提到了

10\. 【強(qiáng)制】序列化類新增屬性時(shí),請(qǐng)不要修改 serialVersionUID 字段,避免反序列失??;如果完全不兼容升級(jí),避免反序列化混亂,那么請(qǐng)修改 serialVersionUID 值。 
說(shuō)明:注意 serialVersionUID 不一致會(huì)拋出序列化運(yùn)行時(shí)異常。 

:這樣其實(shí)也存在一個(gè)問(wèn)題,我們?yōu)榱思嫒葜豢紤]了老版本用戶的serialVersionUID是6163620063913001067L,但是已經(jīng)增加tempDescribe變量的灰度用戶的serialVersionUID還是被自動(dòng)生成為8478373576351162083L,所以當(dāng)這部分灰度用戶升級(jí)到最新修復(fù)的全量版本時(shí),還是會(huì)出現(xiàn)InvalidClassException,報(bào)錯(cuò)為:

Caused by: java.io.InvalidClassException: com.xxx.DeskPushDetailsBean; local class incompatible: stream classdesc serialVersionUID = 6163620063913001067, local class serialVersionUID = 8478373576351162083

這時(shí)只能在反序列化的地方try catch捕獲一下了。

綜上,解決辦法就是:保持serialVersionUID的值不變 + 反序列化的地方try catch捕獲

驗(yàn)證

編寫測(cè)試類SerializableDemo1.java,將DeskPushDetailsBean的值序列化(文件輸出流)保存到當(dāng)前工程的tempFile文件中,再通過(guò)SerializableDemo2從tempFile文件中讀取(反序列化過(guò)程)出來(lái),看是否會(huì)拋出異常;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
/**
 * @author
 * @since 2020/12/27 16:42
 */
public class SerializableDemo1 {
    public static void main(String[] args) {
        DeskPushDetailsBean deskPushDetailsBean = new DeskPushDetailsBean();
        deskPushDetailsBean.pushType = "xzbTest";

        ObjectOutputStream objectOutputStream = null;
        try {
            objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));
            objectOutputStream.writeObject(deskPushDetailsBean);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
//            close;
        }
    }
}
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
/**
 * @author
 * @since 2020/12/27 17:08
 */
public class SerializableDemo2 {

    public static void main(String[] args) {
        File file = new File("tempFile");
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(file));
            DeskPushDetailsBean deskPushDetailsBean = (DeskPushDetailsBean) ois.readObject();
            System.out.println(deskPushDetailsBean);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
//            close;
          
        }

    }
}

執(zhí)行SerializableDemo2類main方法,發(fā)現(xiàn)正常打印deskPushDetailsBean對(duì)象值,說(shuō)明反序列化成功;

image.png

任意更改serialVersionUID為其它值后驗(yàn)證,發(fā)現(xiàn)都會(huì)導(dǎo)致反序列化失敗,拋出InvalidClassException異常,同學(xué)們可自行驗(yàn)證一下。

----------------------------------------結(jié)束分割線-----------------------------------

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

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

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