本文要解决的问题:从 json 字符串反序列化形如 List<BaseItem> 的列表,做采集功能模板化时遇到的问题。
1. 需求
App 端需要采集小区、房屋等实体的信息。采集功能是模板化的,配置文件采用 json 格式。整个模板配置包含许多异构的 json 数组,解析成 Java 对象是 List<>。
例如,一个采集任务可能包括多个页面(List<Page>),每个页面有多个卡片(List<Card>)。Page 和 Card 都是接口,有多种类型的子类,这种列表是异构的列表。
以卡片为例,为了将不同类型的卡片放在同一个列表里,需要抽象出一个公共接口(或基类),卡片被抽象为接口 Card,不同类型卡片分别实现这个接口。以下面简化的配置文件为例,它对应 List<Card>( List<Card> 作为 TemplateDocument 类的成员)
1 | { |
反序列化后的 List<Card> 中的元素如下:
1 | +------------------+ |
TemplateDocument 的定义:
1 | public class TemplateDocument { |
那么问题来了,App 拿到这段 json 该如何还原为原来的 List<Card> 呢?
2. 方案
手工解析显然不是我们的追求,在我们的项目中目前有 9 种像 List<Card> 这样的异构列表,并且 List<Card> 通常是更大的数据结构的一部分,无法单独拎出来解析。所以必须找到简便的方案。
注意到每个卡片都有 card_type 字段标识卡片的类型。实际上,这是定义配置文件格式时有意规定的,通过这种方式,将卡片类型编码在 json 字符串里,反序列化时根据这个字段的值,就知道对应哪种实体 Bean 了。
我们的项目使用 Gson 库作为 json 的序列化方案。Gson 支持自定义任意类型的序列化、反序列化。相关接口如下:
1 | public final class GsonBuilder{ |
这三个方法中,基础是 registerTypeAdapterFactory,另外两个是对它的封装。我们需要注册 Card.class 的 TypeAdapter。
Object类型的参数typeAdapter可以是 TypeAdapter, JsonSerializer, JsonDeserializer 或 InstanceCreator,前三个与我们的目的有关,JsonSerializer 和 JsonDeserializer 官方不建议使用,使用 TypeAdapter 更高效。我们使用基础的 registerTypeAdapterFactory 函数,另外两个方法的参数TypeAdapter拿不到我们需要的 gson 对象。
3. 实现
3.1. 实现 TypeAdapterFactory
TypeAdapterFactory 是生成 TypeAdapter 的工厂,这个接口只有一个方法,根据类型创建对应的 TypeAdapter,在这里 create() 的参数 type 就代表 Card.class:
1 | <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type); |
如果 type 是我们要处理的类型,则返回一个新的 CardTypeAdapter 实例,否则返回 null 表示不能处理这种类型。
3.2 实现 CardTypeAdapter
CardTypeAdapter 继承自 TypeAdapter,需要实现 write() 和 read() 方法。分别是 Card 类型(子类)的序列化和反序列化过程。
write 过程无需干预,代理给 Gson 内置的对应类型(Card 的子类)的 TypeAdapter 就可以了。如何获取 Gson 对某种类型的默认 TypeAdapter 呢,Gson 提供了接口:
1 | public class Gson { |
TypeToken 使用 TypeToken.get() 获取。例如,获取 TextInputCard.class 的 TypeAdapter:
1 | TypeAdapter<TextInputCard> delegateAdapter = gson.getDelegateAdapter(factory, TypeToken.get(TextInputCard.class)); |
read 过程的处理需要一些技巧。read() 的函数原型如下:1
public abstract T read(JsonReader in) throws IOException;
参数 JsonReader 对应一个卡片序列化后的 json 信息,我们需要读取 card_type 字段,根据它的值交给对应子类的 TypeAdapter去处理。
JsonReader 提供的接口是流式(stream)的,无法直接根据字段名称(card_type)拿到对应的值。先转成万能类型 JsonObject(注意是 com.google.code.gson 包下的,不是 org.json.JSONObject ),然后就可以拿到 card_type 的值了,接着也可以方便的把 JsonObject 转成对应的子类对象。
1 | TypeAdapter<JsonObject> jsonObjectTypeAdapter = gson.getAdapter(JsonObject.class); |
拿到 cardTypeName 后,查找对应的子类的代理 TypeAdapter,调用其 fromJsonTree 方法就可以了。
查找子类时有一个小问题,由于 Java 的强类型特性,直接用 Map<String, TypeAdapter<? extends Card>> 保存对应关系将在某处出现类型不兼容问题,处理方法是增加一个适配层,具体方法见开源出来的代码中的 TypeReadWriteAdapter 类。
3.3 抽象为可复用的库
上面讲的是针对 Card.class 这个特例,使用 Java 泛型技术可以抽象为可复用的库。抽象后使用起来就方便多了。上面的例子现在只需要分别继承 BaseTypeAdapterFactory 和 BaseGenericTypeAdapter,调用 registerSubtypeAdapter 注册类型名称与实现类的映射就可以了。实现 CardTypeAdapterFactory 和 CardTypeAdapter 只需要 20 行代码。
以下是新的 CardTypeAdapterFactory 实现:
1 | public class CardTypeAdapterFactory extends BaseTypeAdapterFactory<Card> { |
使用
1 | final GsonBuilder builder = new GsonBuilder(); |
注: 考虑到实际使用场景,遇到未知的类型值,将会忽略(并输出 Error 级别的 log),以便旧版的 App 兼容新版本的配置。这时,List<Card> 将出现元素为 null 的情况,使用时需要注意。
4. 总结
借助 Gson 提供的扩展能力和 Java 泛型技术,我们实现了类型安全的反序列化异构列表的工具,让反序列化异构列表变得像使用 Gson 本身一样简单。
这个技巧在项目的模板化中得到了大量运用,目前共有 9 个这样的异构列表,模板配置文件有 597 行,解析模板不是一件痛苦的事情。对应的保存采集结果的 json 结构与此类似,但大小可达上万行(格式化后的 json)。曾经遇到过一个压缩后大小 294KB,13818 行的 json,使用这个方法也顺利的反序列化出来。
模板化能够快速响应新的采集需求,配以服务端动态下发模板配置,大大提高了产品的灵活性,缩短了需求的响应时间。其他有需要做模板化的产品可以参考这里提供的方案。
抽象出来的库和文中的例子已经开源在 https://github.com/wuairc/heterogeneous-json-list,欢迎使用和提出建议。