今天帮同事查了个关于 fastjson 反序列化的问题,现象是一处反序列化的代码在发布至测试环境一段时间后反序列化的结果不正确,且该问题在本地无法复现。经过排查,确认与泛型的反序列化有关,该组应用的 fastjson 版本为 1.2.29,简化相关代码后有如下类:
1 | package me.tianshuang; |
即支持泛型的类:ApiResult<T>。同时我们还有如下实体类 BizDTO:
1 | package me.tianshuang; |
我们编写如下的测试代码:
1 | package me.tianshuang; |
执行后输出如下:
1 | ApiResult{data={"key1":"value1","key2":"value2"}} |
即我们反序列化未使用泛型的 ApiResult 数据时,能够正确地反序列化,且此时反序列化出的 ApiResult 实例中 data 字段的类型为 JSONObject。我们稍微改动以上的测试代码:
1 | package me.tianshuang; |
即先对使用了泛型的 ApiResult<BizDTO> 数据进行反序列化,然后再对未使用泛型的 ApiResult 数据进行反序列化,此时输出如下:
1 | ApiResult{data=BizDTO{code='This is code', token='This is token'}} |
到此处一切正常,我们再次改动以上的测试代码:
1 | package me.tianshuang; |
即在对使用了泛型的 ApiResult<BizDTO> 数据进行反序列化之前加上了一次对未使用泛型的 ApiResult 数据的反序列化,程序输出如下:
1 | ApiResult{data={"key1":"value1","key2":"value2"}} |
此时问题出现了,最后一次反序列化时 data 字段的类型识别出错,本该反序列化为 JSONObject 类型的,但是反序列化为了 BizDTO 类型,导致 data 字段的数据错误。
我们跟踪 JSON.parseObject() 方法调用可以确认转换配置均使用的 ParserConfig.global 静态实例,且为每个传入的字符串创建了一个 DefaultJSONParser 实例,部分源码如下:
1 | public static <T> T parseObject(String text, Class<T> clazz) { |
ParserConfig.global 静态实例位于 ParserConfig.java at 1.2.29:
1 | public static ParserConfig global = new ParserConfig(); |
即在多次 JSON 反序列化期间,共用的 ParserConfig.global 这个转换配置。回到我们的测试代码 TestA,容易发现为 ApiResult.class 创建了一个类型为 JavaBeanDeserializer 的对象反序列化器,触发该反序列化器创建的代码位于 ParserConfig.java at 1.2.29:
1 | return new JavaBeanDeserializer(this, clazz, type); |
此时 clazz 与 type 为同一个实例,即 ApiResult.class,创建 JavaBeanDeserializer 实例的代码位于:JavaBeanDeserializer.java at 1.2.29:
1 | public JavaBeanDeserializer(ParserConfig config, Class<?> clazz, Type type){ |
即根据 JavaBeanInfo.build() 解析出的 JavaBean 信息创建 JavaBeanDeserializer 对象,JavaBeanInfo.build() 的源码位于:JavaBeanInfo.java at 1.2.29,因为方法体较长,此处未粘贴源码。即通过 JavaBeanInfo.build() 解析完 ApiResult 类后,使用解析的信息创建出用于反序列化 ApiResult 类的 JavaBeanDeserializer 对象,且在此过程中,还会为 ApiResult 类中的每个字段创建出对应的反序列化器。然后调用 ParserConfig.java at 1.2.29 中的如下代码将 ApiResult 类的反序列化器缓存至 deserializers 中:
1 | private final IdentityHashMap<Type, ObjectDeserializer> deserializers = new IdentityHashMap<Type, ObjectDeserializer>(); |
deserializers 存储的为 Type 子类实例与 ObjectDeserializer 实例的映射,注意 Type 为 Java 中所有类型类的超接口,其源码定义位于 Type.java at jdk8-b120:
1 | package java.lang.reflect; |
常见的 Class 类就实现了 Type 接口,源码位于 Class.java at jdk8-b120:
1 | public final class Class<T> implements java.io.Serializable, |
我们定义的 ApiResult 实例对应的类型即为 Class 的实例,可以用 ApiResult.class 字面量表示。而 ApiResult<BizDTO> 实例对应的类型为 ParameterizedTypeImpl 的实例,源码位于 ParameterizedTypeImpl.java at jdk8-b120:
1 | public class ParameterizedTypeImpl implements ParameterizedType { |
其实现的 ParameterizedType 接口中实现了 Type 接口。注意 deserializers 使用的数据结构为 IdentityHashMap,不过此处的 IdentityHashMap 非 JDK 原生实现,我大致看了下,该 IdentityHashMap 可以看作是 JDK 中 IdentityHashMap 与 HashMap 的混合体的精简版,如:移除了扩容机制,移除了哈希的防御式函数,沿用 HashMap 的 Entry 数组而未使用 JDK IdentityHashMap 中的 Object 数组等。可以认为该 IdentityHashMap 是为 fastjson 定制的基于引用的哈希表实现,源码位于:IdentityHashMap.java at 1.2.29。
回到我们之前的逻辑,我们知道在测试代码 TestA 中,对未使用泛型的 ApiResult 数据进行反序列化时,会为 ApiResult 类型创建一个 JavaBeanDeserializer 反序列化器并缓存起来,然后对数据进行反序列化。且在 JavaBeanDeserializer 反序列化器的创建过程中,会为 ApiResult 类中的每个字段创建一个字段反序列化器,其中为 T data 字段创建了一个 DefaultFieldDeserializer 实例,然后在对 data 字段反序列化的过程中,会设置 DefaultFieldDeserializer 实例的 fieldValueDeserilizer 字段为全局的 JavaObjectDeserializer 实例,因为此时 data 字段对应的 fieldInfo.fieldClass 为 java.lang.Object,fieldInfo.fieldType 为 T,所以从反序列化器缓存 deserializers 中获取到 java.lang.Object 对应的全局反序列化器 JavaObjectDeserializer.instance,该映射在 ParserConfig 实例化时被写入 deserializers,源码位于:ParserConfig.java at 1.2.29:
1 | private final IdentityHashMap<Type, ObjectDeserializer> deserializers = new IdentityHashMap<Type, ObjectDeserializer>(); |
然后通过该 JavaObjectDeserializer 实例的 deserialze 方法调用 parser.parse(fieldName) 进行 data 字段的反序列化,DefaultFieldDeserializer 中 parse 方法的部分源码位于 DefaultJSONParser.java at 1.2.29:
1 | public Object parse(Object fieldName) { |
此时会根据 data 字段出现的 { 创建出 JSONObject 实例并进行反序列化,继续执行代码即可完成 ApiResult 数据的反序列化,以上是测试代码 TestA 的核心处理逻辑。现在我们再看看测试代码 TestB,该段测试代码在反序列化未使用泛型的 ApiResult 数据的逻辑之前增加了反序列化使用了泛型的 ApiResult<BizDTO> 数据的逻辑,那么此时的处理逻辑有何不同呢?
在测试代码 TestB 中,首先对使用了泛型的 ApiResult<BizDTO> 数据进行反序列化,因为此时传入的 type 为 new TypeReference<ApiResult<BizDTO>>(){}.getType(),即为 ParameterizedTypeImpl 的实例,且此时 deserializers 中不含有 ApiResult<BizDTO> 类型与 ApiResult 类型对应的反序列化器,所以此时会为 ApiResult<BizDTO> 类型创建一个 JavaBeanDeserializer 并放入 deserializers 缓存中,注意此时放入的 key 为 ParameterizedTypeImpl 的实例,即 ApiResult<BizDTO> 对应的类型,且此时对 data 字段解析出的 fieldInfo.fieldClass 与 fieldInfo.fieldType 均为 BizDTO,然后会为 data 字段使用 ASM 生成一个 BizDTO 的反序列化器,并赋值至 fieldValueDeserilizer,随后再执行反序列化相关操作,以完成对 ApiResult<BizDTO> 数据的反序列化。接着对未使用泛型的 ApiResult 数据进行反序列化,在反序列化过程中,会去 deserializers 中查找缓存的反序列化器,此时查找的 key 为 ApiResult.class,无法找到缓存的反序列化器,所以会为 ApiResult 创建一个反序列化器并执行后续的反序列化操作,测试代码 TestB 按照预期执行。
我们再看测试代码 TestC,该段测试代码的执行逻辑为:先对 ApiResult 数据进行反序列化,再对 ApiResult<BizDTO> 数据进行反序列化,再对 ApiResult 数据进行反序列化。此时在最后一次反序列化过程中,输出了错误的结果。根据我们之前的分析知道,每次反序列化时会先查询当前类型的反序列化器,如果没有反序列化器,则创建反序列化器,我们再看看获取反序列化器的逻辑 ParserConfig.java at 1.2.29:
1 | private final IdentityHashMap<Type, ObjectDeserializer> deserializers = new IdentityHashMap<Type, ObjectDeserializer>(); |
我们知道 deserializers 是 Type 子类实例与 ObjectDeserializer 子类实例的映射,这里面存储了为各个 Type 子类实例创建的反序列化器,如果我们查找的 Type 子类实例不存在反序列化器,则在以上的代码逻辑中,会为该 Type 子类实例创建反序列化器并放入 deserializers,如以上代码中的 154 行。注意此时的 key 为 type。我们再回顾下测试代码 TestC:
1 | package me.tianshuang; |
在第 12 行,我们对未使用泛型的 ApiResult 数据进行了反序列化,在此过程中,程序往 deserializers 写入了 ApiResult.class 与其反序列化器的映射,然后执行到 14-15 行时,会去 deserializers 中查找是否有匹配的反序列化器,根据上方获取反序列化器的代码我们知道,首先会使用 type 进行查找,即根据 new TypeReference<ApiResult<BizDTO>>(){}.getType() 查找是否有对应的反序列化器,此时是没有的,因为此时查找的 key 为 ParameterizedTypeImpl 的实例,而 12 行为缓存中写入的为未使用泛型的 ApiResult.class 的反序列化器,然后调用获取反序列化器代码片段中的第 14 行 return getDeserializer((Class<?>) rawType, type); 继续查找,此时 rawType 为 ApiResult.class,type 为 ApiResult<BizDTO>,在 getDeserializer(Class<?> clazz, Type type) 方法实现中,会优先根据 type 查找,如果找不到,则根据 clazz 查找,那么在此过程中,最后会通过反序列化器查找代码片段中的第 49 行 derializer = deserializers.get(clazz); 查找到 ApiResult.class 的反序列化器,接着使用该反序列化器对字符串进行反序列化,在对 data 字段进行反序列化的过程中,在 DefaultFieldDeserializer 类的 parseField 方法中,存在如下逻辑 DefaultFieldDeserializer.java at 1.2.29:
1 | protected ObjectDeserializer fieldValueDeserilizer; |
最为关键的即为第 16 行,此时 fieldType 为 BizDTO,第 16 行会为 BizDTO 创建一个反序列化器并赋值给成员变量 fieldValueDeserilizer,这就导致 data 字段对应的 DefaultFieldDeserializer 实例中的 成员变量 fieldValueDeserilizer 被修改了。然后在最后一次反序列化调用时,即使反序列化的是未使用泛型的 ApiResult 数据,但是 data 字段对应的 DefaultFieldDeserializer 实例中的成员变量 fieldValueDeserilizer 已经为 BizDTO 的反序列化器了,那么使用 BizDTO 反序列化器去反序列化 {"key1":"value1","key2":"value2"} 这样的数据时,造成了文首提到的反序列化结果不正确的现象。这也与同事反馈的现象相吻合,即本地测试反序列化未使用泛型的 ApiResult 数据正常,发布到测试环境一段时间后反序列化结果不正常,因为在这段时间内有其他代码执行了对 ApiResult<BizDTO> 的反序列化操作。
随即我查询了 DefaultFieldDeserializer 的提交记录,发现该问题已经在 bug fixed for generic type deserialize. · alibaba/fastjson@ff078fb · GitHub 这一次提交中被修复了,而关于该组应用使用的 fastjson 版本不是最新的问题,是我们发现 fastjson 在升级过程中存在破坏了向后兼容性的问题,一旦升级,影响较大,其在文档中也给出了说明:incompatible_change_list · alibaba/fastjson Wiki · GitHub。
确认了原因后,解决办法总是很简单,此处不再赘述。在排查该问题之前,我并不了解 fastjson 的反序列化实现,但是根据经验,在跟随代码执行流程走一遍后就能大致理解其实现逻辑,再多次编码验证复现该问题并定位问题原因。
References
GitHub - alibaba/fastjson: A fast JSON parser/generator for Java.
IdentityHashMap
Chapter 15. Expressions - 15.8.2. Class Literals