

这个字段我明明传了呀,为什么收不到 - Spring 中首字母小写,第二个字母大写造成的参...
source link: https://www.cnblogs.com/techcorner/p/17391439.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

这个字段我明明传了呀,为什么收不到 - Spring 中首字母小写,第二个字母大写造成的参数问题
vSwitchId
、uShape
、iPhone
... 这类字段名,有什么特点?很容易看出来吧,首字母小写,第二个字母大写。它们看起来确实是符合 Java 中对字段所推崇的“小驼峰命名法”,即第一个单词小写,后面的单词首字母大写。但是,如果你在项目中给 POJO 类的字段以这种形式进行命名的话,那么可能会碰到 序列化/反序列化 的问题。。。下面就是一个我在项目中亲自踩过的坑
Spring Web 开发中,我们往往使用 POJO 对象来充当请求传递时的 body。例如现有一个用于传输的 POJO 对象,我将其进行简化后如下
@Data
public class InstanceRequest {
private String vSwitchId;
}
然后在 Controller 中使用这个对象作为 @RequestBody 来获得请求体,并在处理逻辑中输出 vSwitchId
字段
@RestController
public class InstanceController {
@RequestMapping("/createInstance")
public String createInstance(@RequestBody InstanceRequest request) {
// do something
System.out.println(request.getVSwitchId());
return "success";
}
运行上述应用后,我信心满满的发送一个 HTTP 请求进行测试,充满信心地认为控制台里会打印我传过去的信息
POST /createInstance HTTP/1.1
Content-Type: application/json
{
"vSwitchId": "xxxx"
}
结果却发现,控制台输出了一个大大的 null。。一脸懵逼,我逐字对比自己发送的 JSON 字段名和类里面的字段名。。v...S...w...i...t...c...h...I...d... 没问题呀,一个字母都不差呀,为什么收不到呢?
vSwitchId
字段为什么没有成功解析到?我们知道 Spring 是通过 jackson 框架来进行序列化和反序列化的,因此需要深入 jackson 的源码,看看为什么这个字段没有被成功反序列化。
深入 Jackson 源码探究原因
Jackon 中,主要通过AbstractJackson2HttpMessageConverter.readJavaType
方法将 HTTP 请求中的消息体转换为对象,因此直接对其打断点进行调试
根据断点逐步推进,进入 ObjectMapper._readMapAndClose
方法
看到这里有 _findRootDeserializer
方法,顾名思义,应该是根据当前想要转换的对象类型,来寻找对应的反序列化器了。那么继续进去看看...
往下层层递进后,找到创建反序列化器的地方,在 DeserializerCache._createDeserializer
里,也就是说是在 DeseializerCache 里面执行创建的步骤,这其实是很常见的 缓存+懒加载 模式:要使用的时候,首先去缓存里面拿,拿不到的时候再创建,创建完直接加入缓存。
在创建反序列化器的方法里,有个 BeanDescription
类值得注意,它指的是类的描述,因此猜测在这个类里面,我们的 POJO 类的字段应该已经被分析完毕了,那么上面的 vSwitchId 到底被分析成了啥,也可以在里面看到。
该类里面有 POJOPropertiesCollector ,那么我们 POJO 类的字段应该是被收集在这个类里面了。
值得注意的是,这是一个懒加载的类,内部的分析逻辑只有在第一次被用到时才会执行。分析逻辑在 POJOPropertiesCollector.collecAll()
这个方法里面。
下面重点就来了,看看这个方法
方法主要逻辑如下:
- 首先初始化了 props,存储所有反序列化过程中需要的属性
- 通过
_addFields(props)
方法从类的字段中抽取属性并加入 props 中 - 通过
_addMethods(props)
方法从类的 getter 和 setter 字段中抽取属性并加入 props 中 - 通过
_removeUnwantedProperties(props)
方法从 props 中剔除掉不想要的属性。哪些属性会被剔除?从代码可以看出,字段、getter、setter 都是私有属性、或者已经被标记为 ignore 的属性,是需要被剔除的。
通过调试发现,执行完 _addFields
后,vSwitchId
字段成功加入
再执行完 _addMethods(props)
后,神奇的事情发生了,加入了另外一个 props vswitchId
接下来,执行 _removeUnwantedProperties(props)
之后
发现 vSwitchId
这个正确的属性已经被剔除了,反而留下了 vswitchId
这个有问题的属性。这是为什么呢?上面提到,_removeUnwantedProperties
会剔除私有的属性,vSwitchId
这个 props 是来自字段的,而字段本身是私有的,因此它被剔除了。
接下来我们需要搞清楚为什么从 getter、setter 中拿到的属性是 vswitchId
而不是 vSwitchId
。
首先,getter 和 setter 是哪里来的?项目中我使用的 Lombok,也就是说 getter 和 setter 是由 Lombok 生成的。在大多数 IDE 中,如果使用 Lombok 生成 setter 方法,它将会被自动隐藏并不会显示在源代码中。如果想要查看生成的方法名称,通常 IDE 会提供一个叫做“Structure”(结构)或“Outline”(大纲)的功能,它可以列出类的所有成员变量和方法,其中也包括由 Lombok 生成的 setter 方法。
可以看到 get 和 set 方法的名称分别是 getVSwitchId
和 setVSwitchId
。接下来看看 Jackson 具体是如何解析方法,从而得到 props 的。相关代码在 DefaultAccessorNamingStrategy.legacyManglePropertyName
中
以上处理流程用大白话解释一下:首先会根据 offset
字段去除前面的三个字母,一般为 get 或 set。去除前面三个字母 'set' 后,发现第一个字母是大写的,因此将第一个字母小写,然后接着往后找,如果后面的还是大写,接着变小写...直到找到了一个本来就是小写的字母后,才将后面所有的字母一股脑添加进来。由于 setVSwitchId
在去除前面的 set 后,前面两个字母都是大写,因此在这种处理逻辑下,最后得到的属性名为 vswitchId
。换句话说,如果 set 方法的名称是 setvSwitchId
,那么处理后得到的就是正确的 vSwitchId
。
说到这里,问题其实就明了了,这个其实是由于 Lombok 生成 getter、setter 方法的语义规范与 Jackson 处理 get set 方法之间的不一致性,导致的属性名无法匹配上的问题。
Lombok
其实在 Lombok 社区里,也有人提出过这个问题,详见 https://github.com/projectlombok/lombok/issues/2693。
可以看出,这个其实是规范的问题,目前没有一个定论。。Lombok 认为自己生成 set、get 方法的规范没有问题,Jackson 那边也认为自己根据 set、get 方法来解析字段名的规范也没有问题,公说公有理,婆说婆有理。。不过,不管是谁有理,最后受到伤害的是我们开发者呀,只要你的项目中同时用到了 Lombok 和 Jackson,就会遇到这个问题。对于没有接触过这个问题的开发者来说,这个问题其实是会平白无故浪费很多时间的。
不过,Lombok 社区还是提出了一个 PR 来解决这个问题,详见 https://github.com/projectlombok/lombok/pull/2996 。
在以上 PR 中,Lombok 社区提供了一个配置项,
lombok.accessors.capitalization = [basic | beanspec] (default: basic)
默认为 basic,也就是 Lombok 默认的行为,会生成 setVSwitchId
这种方法名。
如果将其修改为 beanspec,那么会保持与 Spring、Jackson 相同的规范, 此时会生成 setvSwitchId
这种方法名。
详情也可以看 Lombok 的官方文档 https://projectlombok.org/features/GetterSetter
其中最后一句话很有意思,“Both strategies are commonly used in the java ecosystem, though beanspec is more common“。这意思是,“我承认 Jackson 那边使用的规范更常用一些,但是我默认还是要坚持我的规范...”。
讲到这里,解决方案其实就出来了。这里介绍三种解决方案吧
使用 Lombok 的配置来解决。在项目根目录下创建 lombok.config
文件,并添加以下配置项即可
lombok.accessors.capitalization = beanspec
利用 IDE、或者手动生成 getter、setter 方法
public String getvSwitchId() {
return vSwitchId;
}
public void setvSwitchId(String vSwitchId) {
this.vSwitchId = vSwitchId;
}
利用 Jackson 的 JsonProperty 注解强行指定属性名
@Data
public class InstanceRequest {
@JsonProperty(value = "vSwitchId")
private String vSwitchId;
}
我自己从这个事件中总结出来了一点经验。在 Java 里面,给类属性取名的时候,以前我想着是只要满足小驼峰命名法就万事大吉,不会有什么问题了。。。现在我知道了,并不是说满足小驼峰就万事大吉了,如果碰到 首字母小写、第二个字母大写 的这种情况,还是要特别注意,尤其是当这个类还被用于序列化/反序列化时,一定要注意其处理的规范性,要写(生成)生成符合 Java Bean 规范的 set、get 方法,否则这个小小的字段在反序列化时会一直困扰着你。。让你一直抓狂 “这个字段我明明传了呀,为什么 Spring 就是收不到”。
Recommend
-
114
为什么明明可以,领导们就是不愿直接把事儿给办了?人神共奋·2017-12-09 11:37我不骗你,只是有些事情,我不告诉你
-
81
-
76
-
58
都是“自然选择”惹的祸。
-
36
编者按:本文来自微信公众号
-
6
《这个日本元气少女明明十分吵闹却过分可爱》#3_哔哩哔哩bilibili_人类一败涂地《这个日本元气少女明明十分吵闹却过分可爱》#340.9万播放 · 1473弹幕2021-06-30 09:00:16 全站排行榜最高第64名
-
4
V2EX › MySQL sql update where set 字段相同,这个要怎么写呢 kangsgo · 17 小时 23 分钟前...
-
5
V2EX › Visual Studio Code vscode launch.json 中 有用过 "sourceMap" 这个字段的吗 jdz · 1 小...
-
6
真的假的? 明明近在眼前,却怎么也找不到Phenix_01(95级)楼主2022-02-04 01:57:04推荐 (9)
-
9
Spring 的 Bean 明明设置了 Scope 为 Prototype,为什么还是只能获取到单例对象? ...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK