Java 的 JSON 库有很多, 本文分析 google 的 Gson 和 alibaba 的 fastjson, 在 Java 泛型场景反序列化的一些有意思的行为. 考虑下面的 JSON 字符串:
- [
- "2147483648",
- "2147483647"
- ]
用 fastjson 在不指定类型的情况下解析, 下面的代码输出啥:
JSON.parseArray(s).forEach(o -> { System.out.println(o.getClass()); });
答案是:
- class java.lang.Long
- class java.lang.Integer
是不是感觉有点儿奇怪, 两个都是数字啊, 居然输出了不同的类型, 原因我们下面细讲. 再看看 Gson, 用 Gson 解析并且不指定泛型类型的话, 下面的代码输出啥:
new Gson().fromJson(s, List.class).forEach(o -> { System.out.println(o.getClass()); });
答案是:
- class java.lang.Double
- class java.lang.Double
这次两个都是 Double 类型, 明明是整数啊, 为啥到 Gson 这里变成 Double 了?
我们试了两个 JSON 库, 解析相同的 JSON 字符串, 得到全然不同的结果. 如果在实际使用的时候, 用这种方式反序列化 JSON, 很容易出现 BUG, 而且是运行时才可能出现, 这种问题一旦出现往往很难排查. 因此为了保证泛型类型 JSON 反序列化的正确性, 一定要明确指定泛型的类型. 下面我们先看看正确的解析应该怎么写, 再深度探讨一下内部的原理.
正确的泛型 JSON 反序列化
fastjson 和 Gson 都提供了泛型类型的反序列化方案, 先来看看 fastjson, 对于上面的 case, 正确的反序列化代码如下:
- JSON.parseObject(s, new TypeReference<List<Long>>(){
- })
- .forEach(o -> {
- System.out.println(o.getClass());
- });
创建一个确定泛型类型的 TypeReference 子类(这里是匿名内部类), 将这个子类传递给 fastjson 以帮助 fastjson 在运行时获得泛型的具体类型信息, 从而实现泛型正确反序列化. Gson 的方案与 fastjson 相同, 或者应该反过来说 fastjson 的方案与 Gson 相同. 大家感兴趣的话可以看看 fastjson 的 TypeReference 和 Gson 的 TypeToken 代码, 基本上 fastjson 就是抄袭 Gson, 连注释都抄了.......Gson 指定泛型类型的反序列化方法如下, 也是创建一个确定泛型类型的匿名子类:
- new Gson().<List<Long>>fromJson(s, new TypeToken<List<Long>>(){
- }.getType())
- .forEach(o -> {
- System.out.println(o.getClass());
- });
由于 fastjson 的方案是来自 Gson, 以下只讨论 Gson 的泛型反序列化原理. 为什么泛型的反序列化显得这么麻烦呢, 非要通过子类化的方式, 不能直接告诉 Gson 泛型的类型吗? 是的, Java 是真的做不到, 其实理由很简单, 泛型类既然是泛型, 意味着就不应该带有具体泛型类型的信息, 因此泛型类的字节码本身就不应该也无法保存泛型类型, 但是子类如果继承一个明确泛型类型的父类(父类是一个泛型类型, Gson 里面就是 TypeToken), 子类必须保存父类的明确类型信息, 通过 Class 类的 getGenericSuperclass 方法能够获得父类类型信息, 该类型信息包含了父类具体的泛型类型信息. 这个方法帮助 Java 的泛型体系完整化了, 是非常重要的一个方法.
类库设计分析
面对相同的设计问题, fastjson 与 Gson 在很多点上的选择不同, 借此机会可以一窥类库设计的思想, 让我们一一来看.
是静态方法还是实例化
使用 Gson 之前, 必须进行实例化, Gson 提供了两种方式: 一种是无参数构造器, 一种是通过 GsonBuilder, 后者能够进行更多的定制, 但无论是哪种方法, 都需要实例化一个 Gson 对象. 但是 Fastjson 使用之前是不需要实例化的, 直接使用 JSON 类的静态方法即可实现 JSON 序列化和反序列化. 这一点上来讲, Fastjson 比较方便, 虽然 Gson 是线程安全的, 可以用 static 变量来声明一个 Gson 实例 (饿汉模式的单例) 然后全局使用, 但是还是比 Fastjson 多了一步. 但是 Gson 这么做的好处是如果序列化 (反序列化) 的定制比较多, 可以在初始化的时候完成复杂的扩展定制, 使用的时候依然保持简单, Fastjson 就需要每次都传递额外的参数来实现. 总体来讲各有优化, Gson 是线程安全的, 大部分场景都是定义全局的静态单例, 用起来跟 Fastjson 差不多. Gson 在这里的选择倾向于希望对外的接口保持一致和简单, 即无论怎么定制逻辑, JSON 的序列化和反序列化就那么几个方法, 内部逻辑可以定制, 但是使用接口不用改变.
数字的默认类型
对于数字类型, 如果没有明确指定类型, Gson 默认都解析成 Double 类型, 而 Fastjson 会根据数字的不同, 解析成 Long,Integer 或者 BigDecimal. 我们在生产中用 Fastjson 就遇到这种问题: 由于集合没有指定泛型类型, 反序列化的时候, 不同大小的数字被反序列化成了不同的类型, 导致业务逻辑出错. 这种未制定类型情况下, 感觉 Gson 的处理更合适一些, 既然未指定类型, 对外的默认类型始终是 Double, 接口对外的心智更稳定.
集合类型反序列化
对于列表类型的反序列化, Fastjson 提供了 parseArray 系列方法, 这样很多情况下可以避免使用 TypeReference, 代码写起来更简单. 但是 Gson 就没有这种方法, 如果需要解析列表, 必须使用 TypeToken<List<Xxx>>, 并没有为列表设置特殊的方法, 这里依然能看到 Gson 希望对外的接口保持一致和简单 , 即便牺牲一点儿方便性.
泛型反序列化
为了解析泛型, Gson 和 Fastjson 都提供了类似的机制(Gson 使用 TypeToken 承载类型, 而 Fastjson 使用 TypeReference 承载类型), 利用子类继承确定泛型类型父类的方式, 获得类型, 区别是 Gson 的接口只接受 Type 类型的参数, 不接受 TypeToken 参数, 这是因为 Type 是 JDK 的自带类型, 这种设计的效果是 Gson 的接口非常简单. Fastjson 的接口可以支持 Type 参数, 也支持 TypeReference 参数.
小结
整体上能明显看出来 fastjson 更多是长出来的, 接口多而全, 应该是不断有人提需求的结果, 而 Gson 是设计出来的, 接口的一致性很强, 高内聚低耦合, 有些时候宁愿牺牲接口的便利性, 也要保证接口对外的一致性, 简单和概念完整, 从设计上我是崇尚 Gson 的设计理念的, 但实际的开发过程更容易演变成 fastjson 的模式, 在中国程序员的地位真的不够高.
参考资料
[1]. fastjson 源代码
[2]. Gson 源代码
[3].
来源: http://www.jianshu.com/p/f0fe42636d19