目录

2025-01-04-spring中,为什么前端明明传了值,后端却接收不到

spring中,为什么前端明明传了值,后端却接收不到

文章目录

问题场景

在进行前后端的联调时,有时候会出现,前端明明传了值,后端接口却接收不到的情况,这种情况常常让人很苦恼,然后就会去仔细对比前后端的参数单词是不是对应上了,也会去检查是不是前端的请求参数格式有问题,又或者是后端接口接收的参数格式有问题,一通检查对比下来,发现都没问题。那究竟是为什么呢?那就继续往下看吧。


问题重现

控制层代码:

    @PostMapping(value = "/test")
    public void test(@RequestBody UserVO userVO) {

        System.out.println("用户代码:" + userVO.getUCode());
        System.out.println("用户名称:" + userVO.getUName());

    }

参数实体类:UserVO

@Data
public class UserVO {

    /**
     * 用户代码
     */
    private Long uCode;

    /**
     * 用户名称
     */
    private String uName;

}

用postman模拟前端调用:

https://i-blog.csdnimg.cn/blog_migrate/1795141c3fde5876f4410aba9d7cf018.png

控制台预期打印结果:

用户代码:12345
用户名称:小明

控制台实际打印结果:

https://i-blog.csdnimg.cn/blog_migrate/0afaf55a1ddeb276c574c675ea8e8b44.png


解决方式

在实体类的属性上方加 @JsonProperty 注解,如下图:

https://i-blog.csdnimg.cn/blog_migrate/59ba47c8513fbd2fd19702fa8301251f.png

然后测试控制台打印结果:

https://i-blog.csdnimg.cn/blog_migrate/d2e090697003d552b0572f29dacf77a7.png


原因分析

首先我们先把实体类复原,并且加上一个新的属性 loginType

@Data
public class UserVO {

    /**
     * 用户代码
     */
    private Long uCode;

    /**
     * 用户名称
     */
    private String uName;

    /**
     * 登录类型
     */
    private String loginType;

}

眼尖的同学可能会发现了,我新加的属性loginType长得是不是跟原来两个属性uCode和uName不太一样,不一样的点在于uCode和uName都是首字母小写,第二个字母大写的单词,而loginType则不然。但是它们三都符合驼峰命名法的规范,对吧。这时候可以猜测,难道是这个原因导致的?

在这里我们先来简单验证下 uCode、uName、loginType 的情况

https://i-blog.csdnimg.cn/blog_migrate/0291450d2416e73801d51bbb477c4f28.png

https://i-blog.csdnimg.cn/blog_migrate/af3d10621569162a12e1c296358881cb.png

通过断点发现, uCode、uName 是空的, loginType 却不是空的

然后我们将 uCode、uName 分别改为 userCode、userName 后再进行测试

@Data
public class UserVO {

    /**
     * 用户代码
     */
    private Long userCode;

    /**
     * 用户名称
     */
    private String userName;

    /**
     * 登录类型
     */
    private String loginType;

}

https://i-blog.csdnimg.cn/blog_migrate/deebdfb550541800e70d204239879780.png

https://i-blog.csdnimg.cn/blog_migrate/31f6297ef8bde242b74da3ba9d314e87.png

这个时候我们就可以得出结论,原因就是 首字母小写,第二个字母大写 的单词的属性是有问题的。

但是我们不禁要问,为啥呢?它这也符合驼峰命名法的规范啊。为什么它就有问题呢?感兴趣的同学可以接着往下看。


原理分析

首先我们要知道,在Spring中,前后端之间数据传输会涉及到数据的序列化和反序列化的操作,并且SpringBoot默认是使用Jackson作为JSON数据格式处理的类库。

序列化:按照指定的格式、顺序等将实体类对象转换为JSON对象;

反序列化:将JSON对象中的字符串、数字等,将其转换为实体对象;

那么现在咱们就来断点调试 Jackson 的源码来看看原因。为方便展示,我将实体类留下 uName、loginType 两个属性

@Data
public class UserVO {

    /**
     * 用户名称
     */
    private String uName;

    /**
     * 登录类型
     */
    private String loginType;

}

开始调试:

Jackon主要是通过抽象类 AbstractJackson2HttpMessageConverterreadJavaType 方法将 HTTP 请求中的消息体转换为对象,所以我们找到这部分代码,对他进行断点调试:

https://i-blog.csdnimg.cn/blog_migrate/b2b0f490f345cc7a056513631500dcfb.png

然后逐步断点,在上图的第192行和第195行,它会调用 ObjectMapper.readValue ,然后断点推进到调用方法的核心地方 ObjectMapper_readMapAndClose 方法

https://i-blog.csdnimg.cn/blog_migrate/0d63b8c35cf25c66003ad10c1ded9f48.png

this._findRootDeserializer(ctxt, valueType); 的大概意思就是根据类型找到反序列化器,注意在这边是先从缓存中取,取到了的话就直接返回了。如果没到下一步断点,在这边你可以清除一下缓存。

https://i-blog.csdnimg.cn/blog_migrate/a0633c1b604d400eddb3a600c338091b.png

然后断点继续推进到创建反序列化器的地方 DeserializerCache._createDeserializer

如果你清除缓存或者重启项目在调用时会直接进入到这个创建反序列化器的地方,你直接在这个方法上打断点就好了

https://i-blog.csdnimg.cn/blog_migrate/0ee79a455639d9f9a9df36a9f51162ec.png

找到上图中第164行的代码, BeanDescription 是类的描述的意思,所有的属性都在这里被解析,然后我们断点进去看看。会进入到 POJOPropertiesCollector.collectAll 方法,就是字面意思,收集所有。方法逻辑详见下图:

https://i-blog.csdnimg.cn/blog_migrate/1d20923f1ea410bcfcde768aeb4a4e7b.png

执行完 this._addFields(props);props 加入了 uNameloginType

https://i-blog.csdnimg.cn/blog_migrate/1dd2605ee0dfb44580badfda46bb08e0.png

执行完 this._addMethods(props); 后发现 props 竟然多了一个 uname

https://i-blog.csdnimg.cn/blog_migrate/9ac0c00b2624dfc8a3369416a8c61677.png

在这里我们点开属性详细去看,会发现 uName 的get和set为空,但是 loginType 的是正常的,并且 uname 这个不知道哪里跑出来的属性的get和set也是不为空的。

https://i-blog.csdnimg.cn/blog_migrate/c098968103d9cf1daa01b70a3827827a.png

https://i-blog.csdnimg.cn/blog_migrate/ea066d058fb2e2af9299e7689c04f980.png

再接着执行 this._removeUnwantedProperties(props); 移除不想要的属性之后,会发现就剩下 loginTypeuname 了,因为 uName 没有get和set。为什么

https://i-blog.csdnimg.cn/blog_migrate/11658cc698943b8c0760d1970c74da87.png

然后props中目前存储的就是 loginTypeuname

现在我们就要弄明白为什么有get/set的是uname而不是uName

首先,在这个例子中我使用的是 @Data 注解,也就是使用的 Lombok,也就是说 getter 和 setter 是由 Lombok 生成的。使用注解的话会将get/set方法隐藏起来,然后我们可以通过IDEA的 Structure 来看,见下图:

https://i-blog.csdnimg.cn/blog_migrate/cbe2a15eb4eac613f631452e54f45dd5.png

那么Jackson 到底是如何解析的,使得解析出来的是 uname ,而不是 uName 。它解析的具体代码在 com.fasterxml.jackson.databind.util.BeanUtil 类中的 legacyManglePropertyName 方法中

https://i-blog.csdnimg.cn/blog_migrate/0de5da8be6e9b850d8e102a0412c1752.png

从上图为我们可以很明显的看到,通过这个方法之后 getLoginType 被解析成 loginType 了。那我们再来看看 uName ,见下图:

https://i-blog.csdnimg.cn/blog_migrate/cda0f884fca13b6814547f5dfb14ad68.png

从上图断点我们可以清晰的看见 getUName 被解析成 uname 了,按照我们正常的思维逻辑的话,loginType和uName都符合驼峰命名法的规范,那么uName对应的get方法解析出来应该是uName啊,为什么变成了uname呢?原因就在于这个 legacyManglePropertyName 方法的处理逻辑,它的逻辑大概是:

1.根据入参offset去除get或者get,然后就剩下UName或者LoginType了

2.然后从第一个字母开始解析,如果第一个字母是大写的,于是就将它转成小写,然后找下一个,如果还是大写,就继续转成小写,直到找到一个小写字母后,就把之后的字母(不管大小写)一起拼接进来。

这样就能解释了:

去除get之后的 LoginType 找到第一个字母是大写,转为小写的 l ,下一个字母是小写的了,就直接把后面的全拼接进来,最终形成了 loginType

去除get之后的 UName 找到第一个字母是大写,转为小写的 u ,下一个字母又是大写,转为小写的 n ,在下一个字母是小写的了,就直接把后面的全拼接进来,最终形成了 uname

如果说这边的 getUName 换成 getuName ,那么解析出来的就是正确的 uName 了。

结论

到这里,我们就可以得出结论了

因为 Lombok 生成 get、set 方法的语义规范与和Jackson 处理 get、set 方法之间的不一致,导致属性名无法匹配上,最终也就导致了前端明明传了参数,后端却接收不到的问题。

扩展

我后面去github的 了解了相关内容,lombook社区是这样描述的:

https://i-blog.csdnimg.cn/blog_migrate/9579b2f4c9e7beece95cbc2afce82a01.png

用网页翻译给他翻译成中文,翻译有些不对,但是能看明白大概意思就行

https://i-blog.csdnimg.cn/blog_migrate/44b3c2893572ae055b7162c48ec09230.png

lombok的大概意思就是: 我就是这样的规范,即使其他的工具框架都改了,我也不改,但是建议你们不要使用首字母小写第二个字母大写的属性名,避免出现问题 ,可能知名度比较高的框架都比较傲娇吧哈哈。

但是lombok还是给出了一个解决方案,加上这个配置项

lombok.accessors.capitalization = [basic | beanspec] (default: basic)

其中basic代表遵循lombok的规范(getUName);beanspec代表遵循Spring、Jackson 的规范(getuName)。默认是basic。

看到这里,我就来总结一下能解决这个问题的三种方案吧

1. 加@JsonProperty注解强行指定属性名

@Data
public class UserVO {

    /**
     * 用户名称
     */
    @JsonProperty(value = "uName")
    private String uName;

    /**
     * 登录类型
     */
    private String loginType;

}

2.不使用lombok,使用IDEA默认生成get/set方法

https://i-blog.csdnimg.cn/blog_migrate/eddf38b7d23578de37f433ba079010a4.png

https://i-blog.csdnimg.cn/blog_migrate/048633e687f31426e4737b958cb15cf3.png

https://i-blog.csdnimg.cn/blog_migrate/1ac52270956928f16791dc514269c9a7.png

https://i-blog.csdnimg.cn/blog_migrate/7bc1a8f62ad55de9f9f4c93342693f86.png

3.加上lombok配置项

lombok.accessors.capitalization = [basic | beanspec] (default: basic)

最后,博主的建议是,尽量不要用这种命名方式,如果非要用,那就加上 @JsonProperty 注解强行指定属性名,这样比较方便。

68747470733a2f2f62:6c6f672e6373646e2e6e65742f71715f33393332373635302f:61727469636c652f64657461696c732f313334343139353233