记一次接口调用字段映射失败问题排查
在写接口的时候遇到一个很神奇的问题,编写一个post接口,在使用包装类接收body的时候发现有个字段映射不上。代码如下
@RestController
public class TestController {
@PostMapping("test")
public TestDto test(@RequestBody TestDto testDto) {
return testDto;
}
}
..........
@Getter
@Setter
public class TestDto {
private String sName;
private String value;
}
TestDto中的value可以正常获取值,但是sName却没值。
根据我多年的开发经验,推测应该是字段名的问题,大家都知道springboot接口反序列化是用的jackson,而jackson又是调用的getter和setter实现的序列化和反序列化,这样咱们大概就有一个方向。直接看一下class文件,看看@getter和@setter注解生成的方法长什么样
看起来感觉没什么问题。
那就只能debug看了,为了方便调试,手动编写getter和setter方法(复制过来),然后在setSName方法上打上断点,结果根本没停,说明根本就没有调用该方法。那我们再试试在setValue方法上打上断点,这次走到了
看一下方法调用,发现前几个都是invoke,没啥用,往前看几个,发现deserializeAndSet方法,没看到什么有用的信息
再往前看一个,看看deserializeFromObject方法里面,发现这么一个判断
看一下_beanProperties
发现这里就是保存类字段的信息地方,但是,嗯?为什么是sname,难怪我们的sName无法映射上。
那我们接下来就找找看,这个_beanProperties到底是怎么来的
在_beanProperties上打上断点,调一下接口,嗯?怎么没停?难不成这个是系统启动的时候就加载好的,重启一下试试。还是没有!再调一下接口试试,这次终于是停了
我们再来看看这properties是哪来的
继续深入
终于看到了我们熟悉的内容,这不就是我们要找的东西吗,发现又是从props来的,再回头
继续在_properties上打上断点,注意一定要把Field access勾选上,不然没办法监听到参数的使用
重启服务,调用接口,然后进入addProperty方法
然后继续反推,发现propDefs
阅读方法后发现数据来源于beanDesc.findProperties(),深入得到
继续在这个新的_properties上打上断点,重启服务,跳过几个无用断点,调试接口
深入方法,发现collectAll方法
看到_addFields和_addMethods方法,感觉终于要接近真相了!
接下来我们就看看collectAll方法到底做了什么
可以看到,在_addFields之后,props里面已经存放了TestDto的两个字段,这个时候sName的key还是对的
进入_addMethods方法,这段代码的逻辑是遍历类里的所有方法,如果方法的入参是0个就尝试从add getter方法,如果方法入参是1个则尝试add setter方法。
进入_addGetterMethod方法,发现如下逻辑,如果方法上没有添加json注解就会尝试从方法名称中解析字段名
进入findNameForRegularGetter方法,发现最终是调用的legacyManglePropertyName解析字段名
进入该方法,终于真相大白,重点看下面这段逻辑
这个方法中和方法名一起传进来的还有getter字符的offset用于去除get前缀,然后得到方法中的属性名,首先将属性名的第一个字符变为小写,如果本来就是小写的话直接返回属性名,如getvalue->value。
如果不是则继续遍历剩余字符,将每一个遇到的大写字符都转化为小写,直到遇到第一个小写的字符,然后返回属性名。如getVAlue->value,
getSName->sname,getURL->url。本来_addMethods方法是为了给每个field添加对应的getter和setter方法的,但是现在由于从getter方法中解析的名称和真实的field不一致,就导致会新增一个该名称的字段,我们的sname就是这么来的
这个时候props里面实际上有三个字段,而sName里面是没有getter,setter方法的,我们的getSName方法被sname字段拿去了。然后在经过_removeUnWantedProperties(props)方法之后,没有getter和setter方法的字段就被移除了!
结论
jackson反序列化的逻辑是,先找到类的成员变量field,然后从getter setter方法中反推属性名,为field和方法添加映射,而jackson反推属性名的逻辑是方法中去掉get的部分后跟着的连续大写字符都转换为小写字符。
写在最后
实际上这也和lombok生成getter方法的逻辑有关,如果我用idea自动生成getter方法的话,这个逻辑和jackson是一致的