今天帮同事查了个关于 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 的反序列化实现,但是根据经验,在跟随代码执行流程走一遍后就能大致理解其实现逻辑,再多次编码验证复现该问题并定位问题原因。
Reference
GitHub - alibaba/fastjson: A fast JSON parser/generator for Java.
IdentityHashMap
Chapter 15. Expressions - 15.8.2. Class Literals