首页 > 其他分享 >这个字段我明明传了呀,为什么收不到 - Spring 中首字母小写,第二个字母大写造成的参数问题

这个字段我明明传了呀,为什么收不到 - Spring 中首字母小写,第二个字母大写造成的参数问题

时间:2023-05-11 16:36:11浏览次数:45  
标签:... Spring 首字母 字段 vSwitchId props Lombok 序列化 方法

问题现象

vSwitchIduShapeiPhone... 这类字段名,有什么特点?很容易看出来吧,首字母小写,第二个字母大写。它们看起来确实是符合 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 方法的名称分别是 getVSwitchIdsetVSwitchId。接下来看看 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 就是收不到”。

标签:...,Spring,首字母,字段,vSwitchId,props,Lombok,序列化,方法
From: https://www.cnblogs.com/techcorner/p/17391439.html

相关文章

  • Android 开发 利用 jq 在 Makefile 里给 output-metadata.json 增加字段
    Makefile:.PHONY:uploadupload:metadata #./push-apk.sh.PHONY:metadatametadata:app/build/outputs/apk/release/output-metadata.jsonapp/build/outputs/apk/release/output-metadata.json:buildupdate-content.txt #在Makefile中插入shell脚本要用双美元......
  • SpringMVC18_SpringMVC获得请求数据5
    一、获得请求参数-请求参数类型 二、获得请求参数-获得基本类型参数  三、获得请求参数-获得POJO类型参数 四、获得请求参数-获得数组类型参数1  五、获得请求参数-获得集合类型参数2  六、获得请求参数-获得集合类型参数3  七、获得请求参数-静态资源......
  • SpringBoot中@ControllerAdvice/@RestControlAdvice+@ExceptionHandler实现全局异常捕
    场景在编写Controller接口时,为避免接口因为未知的异常导致返回不友好的结果和提示。如果不进行全局异常捕获则需要对每个接口进行try-catch或其他操作。 可以对Controller进行全局的异常捕获和处理,一旦发生异常,则返回通用的500响应码与通用错误提示。并将异常发生的具体的......
  • spring
    问题idea创建maven项目过慢使用阿里云镜像在maven的conf\settings.xml中新加一个阿里云的镜像地址:<mirror><id>alimaven</id><mirrorOf>central</mirrorOf><name>aliyunmaven</name><url>http://mav......
  • wordpress 为自定义类型文章新增自定义字段
    wordpress强大之处在于有很强的可自定义性,使得插件、主题的开发变得及其便利。就拿我们今天要说的自定义文章添加自定义字段来说,就很便捷。        比如我们要录入一个客户信息到wordpress中,那么需要的字段可不仅仅是什么标题、内容、摘要这么简单了,我们可能需要录入客户......
  • springboot跨域问题解决方案
    以下内容仅供自己学习使用,侵权私聊必删。在进行前后端交互的时候,往往会遇到以下的跨域问题。那么解决这种跨域的话,可以使用以下这种方法:(引自于程序员青戈)创建config配置目录新建CorsConfig类然后把下面的内容复制进去根据自己需要修改以下就可以解决跨域问题啦importo......
  • SpringBoot整合规则引擎Drools
    目录1整合规则引擎Drools1.1前言1.2pom.xml1.3Drools配置类1.4示例Demo1.4.1添加业务Model1.4.2定义drools规则1.4.3添加Service层1.4.4添加Controller1.4.5测试1.5drools规则解析1.5.1简介1.5.2规则体语法结构1.5.3注释1.5.4Pattern模式匹配1.5.5比较操作符1.5.......
  • springboot集成springSwagger生成接口文档
    1.首先引入pom.xml依赖<!--SwaggerAPI文档--><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version><exclusions><exclus......
  • spring aop MethodSignature = (MethodSignature) joinPoint.getSignature();
    MethodSignaturesignature=(MethodSignature)joinPoint.getSignature();为什么这里可以转回为MethodSignatrue,官网上没有这么说,如果这里转换失败,那运行会报错,这是不允许的。必须找到调用这里的代码,看看这里的Signature 是怎么放进去的。网上找了好久没有找到相关知识......
  • java基于springboot+vue的房屋租赁租房系统、租房管理系统,附源码+数据库,免费包运行,适
    1、项目介绍java基于springboot+vue的房屋租赁租房系统、租房管理系统,分为管理员和用户。用户的功能有:登录、注册、房屋信息、交流论坛、房屋咨询、在线客服、个人中心、我的收藏、我的发布、预约看房管理、在线签约管理、租赁评价管理、管理员的功能有:登录、个人中心、用户管......