反序列化引起的线上问题思考

背景:周一早上例行巡检,发现有一个crash出现两次,刚好新版本刚灰度10%比例。立即暂停灰度,确认问题影响范围。

一、问题表现

1. bugly上对应crash异常上报如下:
企业微信截图_36784770-628a-4de5-bdda-bc4392f0a97a.png
2.定位代码位置如下
企业微信截图_c720b4ef-aad0-4c47-a038-f72893451de8.png

代码有部分直接马赛克,非常简单直接的代码一个data对象,实现序列化接口,里面有定义序列号。成员变量set集合直接创建对象,并给默认初始值。提供一个get方法返回这个set.

并没有地方对这个对象赋值为null,照理说不应该存在为空的场景。 如果是自己遇到这个问题也可以看看自己分析思路、方向。

3.问题原因

上面的data对象做了本地持久化存储。set字段是新版本需求加的。历史版本有缓存一个对象在本地。覆盖升级安装时,缓存数据反序列化,因为老版本没有set字段,反序列化后 set会变成null.
这部分历史代码还没有用KT,所以外面在调用时,如果没有非空判断。就存在crash问题。

实际开发时候,多人协作时这种历史代码不是自己开发不了解它的使用逻辑时,很容易出现这种问题。而且测试不容易发现。因为启动时候会请求服务器数据更新缓存。出现crash用户刚好是启动阶段请求服务器接口慢,在使用到数据反序列化时还没有请求回来数据更新缓存出现。

我正常去某个对象加个字段,并且成员变量上来就new出来的对象怎么会空呢???
大部分使用场景都是直接new 对象使用,肯定不会出现空。但是如果有持久化存储这种序列化对象,在反序列化时候上面的代码就是可能为空,所以使用时注意判断

4.问题复现

安装老版本,抓包看到请求数据后,覆盖安装新版本,mock接口延迟返回。就能复现,异常和bugly一致。

5.问题修复

问题修复就很简单了,因为暂停了灰度,需要快速修复上线继续灰度放量,直接获取方法里面判断如果为空 创建对象返回。
后续修复有几个方向:1.反序列化时增加逻辑去兜底 开发者不用关注需要做这种兜底 2.增加一些探测机制

二、序列化反序列化知识点梳理

遇到这个问题时,除了去看历史逻辑不熟悉耗费时间,发现序列化反序列化相关很多知识点已经比较模糊了,整个问题确认验证比较占用时间。梳理下相关知识点巩固下,也可以自己看下下面的问题是不是都非常清楚。

1.反序列化后的对象会重新调用构造函数吗?
答:不会, 因为是从二进制直接解析出来的

2.序列化与反序列化后的对象是什么关系?
答:是一个深拷贝, 前后对象的引用地址不同

3.序列化ID是什么,有什么用?不用有什么影响?更改了有什么影响
serialVersionUID 是一个 private static final long 型 ID, 一般是对象的哈希码,可以使用 serialver 这个 JDK 工具来查看序列化对象的序列化ID。
用于对象的版本控制。也可以在类文件中指定 serialVersionUID。不指定serialVersionUID时,当你添加或修改类中的任何字段时,则已序列化类将无法恢复,因为为新类和旧序列化对象生成的 serialVersionUID 将有所不同。Java 序列化过程依赖于正确的序列化对象恢复状态的,并在序列化对象序列版本不匹配的情况下引发InvalidclassException

4.针对反序列化场景新增加字段有没有方法可以规避出现空指针场景?
从上面的描述我们知道,如果缓存了一份老版本的数据在本地,新版本修改了字段,直接从老版本缓存反序列化数据使用,可能出现为空场景。大项目协作时,从架构上避免出现上述问题。可以采用一些补全的实现,比如: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;
    }

执行一样的操作,先缓存一份到本地,然后下次反序列数据,新增加的字段比如 HashSet<String> set = new HashSet(); 的set就会出现空,使用时候没注意就空指针了。 但是如果使用下面的方法,使用JSON转换,它会自动补全set反序列化出来是一个空数组,不是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.序列化时如果某个成员变量不序列化怎么处理?反序列化后是什么样的?
如果你不希望任何字段是对象的状态的一部分,然后声明它静态或瞬态根据你的需要,这样就不会是在 Java 序列化过程中被包含
反序列化后对应字段是null,因为序列化时候根本没存进去

6.某个对象中有一个内部对象未实现序列化,序列化时候会出现什么
运行时将引发不可序列化异常 NotSerializableException

7.如果当前类是可序列化,但父类不是,父类的成员反序列化后如何
反序列化后为空

8.是否可以自定义序列化?怎么做
对于序列化一个对象需调用 ObjectOutputStream. writeObject(saveThisObject),并用ObjectInputStream. readObject()读取对象,但 Java虚拟机为你提供的还有一件事,是定义这两个方法。如果在类中定义这两种方法,则JVM将调用这两种方法,而不是应用默认序列化机制。
你可以在此处通过执行任何类型的预处理或后处理任务来自定义对象序列化和反序列化的行

9.序列化源码看过吗?主要用到哪些,枚举特殊吗?
readOb jectO 的用法、vriteObject O、readExternal () 和 writeExternal0•Java 序列化由java.io. ObjectOutputStream类完成。该类是一个筛选器流,它封装在较低级别的字节流中,以处理序列化机制。要通过序列化机制存储任何对象,我们调用
ObjectOutputStream. writeObject(savethisobject),并反序列化该对象,我们称之为ObjectInputStream.readObjectO方法。调用以 writeObject( 方法在 java 中触发序列化过程。
关于 readobject()方法,需要注意的一点很重要一点是,它用于从持久性读取字节,并从这些字节创建对象,并返回一个对象,该对象需要类型强制转换为正确的类型

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容