fastjson反序列化出现的参数丢失问题

2,064 阅读4分钟

今天遇到一个问题:consumerA 调用 providerB 的一个方法,参数为封装类型,其中有个参数x,假设参数名叫isMogujie,类型为基础类型boolean。

providerB 提供了新的jar包,将字段x类型由基础类型boolean升级为封装类型Boolean,providerB升级发布,consumerA没有升级。

之前想当然的以为两边字段名保持一样就行了,序列化协议应该是用反射什么的直接给field赋值,然后出现了问题:providerB拿不到这个布尔参数的值。

排查思路:

1.首先怀疑是两端参数提供的get set 方法名称不一致导致,因为类上使用了lombok,lombok对于同一个名为isXXX的字段所提供的getset方法的javabean规范是有一些区别(应该是lombok为了使方法名语义上更贴切,基础类型的字段一定是true或false,所以直接去掉了set里的is和get,但封装类型有null,所以区别对待):

基础类型:

set:setMogujie()

get:isMogujie()

封装类型:

set:setIsMogujie()

get:getIsMogujie()

(穿插一下,idea自带的Javabean规范又是另一种。不要再用is开头的属性了,实在不行用个0、1枚举代替一下什么的)

基础类型:

set:setMogujie()

get:isMogujie()

封装类型:

set:setMogujie()

get:getMogujie()

区别很明显,但是思考一下像是fastjson的javabean规范和lombok的不一致导致,也就是说应该是序列化时调用的get方法或者反序化时调用的set方法导致consumer的字段值没取到或provider的字段未被赋值,但大概率不会是前者,因为线上一直都是基础类型而且正常。

2.根据上面的结论,先把consumer的版本升上来,调用一次。

先抓包看了consumer调用的情况:

可以看到有值,所以上面猜测javabean规范的原因应该不成立。再debug一下provider:

入参有值。所以看起来是上面猜的 两端get set方法名不一致导致。

3.为了证明是不是因为getset不一致导致,我把consumer版本升上去,把provider版本降下来。也就是和出问题时的场景完全相反,consumer封装类型,provider基本类型,看看结果:

provider仍然可以拿到:

这样基本是看不出来根本原因了。得看一下序列化和反序列化具体内部是怎么操作的。tesla默认使用fastjson作为序列化协议,详情看com.mogujie.tesla.network.Json4jSerialization,我就直接进fastjson里dubug看看。

构造了一个简单的类

先看序列化,先遍历所有的方法,通过方法名去找属性,is开头的去掉了is:

然后找属性发现是null,再用方法名去找,也就是isXXX,找到对应属性:

最后组装了一个name是不带is的fieldInfo,推测这个name最终作为序列化的字段名:

get开头的方法直接去掉get作为propertyName:

最后组装了一个name带is的fieldInfo:

发现fastjson是用方法去找属性,对上面的类我如果新增一个get方法,两个字段序列化后就会出现三个key:

用真实数据验证一下序列化的结果如下:

所以把上面四种情况列出来如下,我们后面反序列化的时候只需要看2、3两种情况为什么结果不一致:

反序列化前再定义一个类,字段名不变,类型相反:

先调用了ASMDeserializerFactory创建类一个目标类类的序列化器,里面可以看到有两个field序列化器,通过名字可以看出来是目标类:

反序列化字段是和序列化相反:根据input的key去找反序列化器里的set方法。具体开始反序列化属性的入口:

中间的过程省略,直接看到差异:

对于is开头的字段,如果pos==-2则会额外执行一段逻辑,这段逻辑成功找到了对应的set方法(具体实现没看):

而不是is开头的属性,如果刚开始没有找到对应的方法就不会去额外再找is开头的set方法了,所以最终出现了这种情况。

结论:直接原因lombok这种特殊的bean规范导致。所以归根结底还是rpc接口尽量不要用基础类型的属性,尤其是布尔类型。

推荐安装一下阿里开发规范插件,这种写法会直接提示报错: