使用 Gson 序列化容器的问题与解决

前两天修改一个项目的代码,其序列化数据的方法中,使用了 Gson 来处理。作为数据的管理类,有一个成员是 List<> 数据类型,经过我的扩充,List 中的元素种类由一种变为了多种,于是对这多种数据进行了抽象,将它们都改造为从同一个基类派生而来,希望能达到只需把 List<> 原来的单一实体数据类改成抽象后的基类即可,无需再改动其他诸如插入、删除等的逻辑。简而言之,若之前的列表形如 List 的话,现在改成 List,而 Data1 以及新出现的 Data2 乃至 DataN 都是 DataCore 的子类。

测试的一开始是没有问题的,既可以在运行时添加和移除,也能正常序列化到永久存储,而且打印出的序列化数据都挺完整,不由得小小自鸣得意一下。不过不和谐的声音很快就出现了,发生在反序列化的过程中,会报出大致为“无法实例化 DataCore 抽象类”这样的问题。一开始颇令人惊讶,毕竟数据的完整性是经过了检查的,在遇到具体派生类的数据时,怎么会去实例化基类呢?

查阅资料才知道,Java 的泛型容器,仅在编译时检查元素类型是否符合,至于每个元素真正的运行时实体类型信息,它是不负责的,这些信息在序列化过程中并没有随每一个元素对象保存下来,而当它作为 List 这样的类型反序列化的时候,它天然认为所有的数据中每一个都应该被再次实例化为一个对应的 DataCore 对象,而非其当初真实的派生类对象。

在知晓上述基础知识后,着手对数据结构进行改进,第一步是把具体类的对象分别存储到独立的 List 中,把类的名字与之关联起来,也进行永久化,这部分简单;第二步是,由于数据管理类本身不希望提前预知共有多少个具体的派生数据类,因此前述的独立 List,仍然写作 List 而不是 List 这样的形式。当反序列化时,每个列表都根据对应的类名,通过其类对象来处理各个元素的实例化工作。

若非如此的话,在 Gson 的常规操作方式下,应该对每一个具体的数据派生类编制并注册对应的 TypeAdapter 序列化适配器来完成。这个方法的弊端就在于,将来的每一个可能的新的数据派生类,都要写一个对应的适配器,略显繁冗。国人有把 Gson 官方使用指南进行了翻译的译文系列,可以参考:

关于对 List 这样的容器相关的序列化,还有一篇英文 blog 也可以参考:Serializing and Deserializing a List with Gson

在 Gson 对一个类的所有数据成员进行序列化的过程中,如何把指定的某个成员排除在外,一度令人厌烦。乍看之下,Gson 有一个注解,正是为此目的而设,即 @Expose(serialize = boooleanValue, deserialize = boooleanValue)。可惜当老夫兴冲冲在一个成员上加上此注解语句自以为完事大吉的时候,测试发现注解无效,该成员仍然被序列化了。查资料才发现,这个注解默认无效!需要提前调用 GsonBuilderexcludeFieldsWithoutExposeAnnotation 方法才会生效。可是,一看这个方法名称,你会第二次懵圈:这个调用确实会导致 @Expose 注解被处理,但是!没有此注解的成员则统统会被忽略!一瞬间,我认为 Google 制定此库的策略的架构师,脑子简直是太反常了。

不过好在跟我同感的前辈并不稀缺,已经有人进行了许多努力来换缓解这一逆天的设定,我最后选择了其中一种,并由此又多少体会到了注解的玩法还是很花的。至于都有哪些解决思路,阅读本文的读者,我强烈建议各位收藏这一篇讨论:Gson: How to exclude specific fields from Serialization without annotations,在其中,程序员们合纵连横,推陈出新,是寻求问题最优解的现实典范。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注