spring中前端明明傳了值后端卻接收不到問(wèn)題解決辦法
問(wèn)題場(chǎng)景
在進(jìn)行前后端的聯(lián)調(diào)時(shí),有時(shí)候會(huì)出現(xiàn),前端明明傳了值,后端接口卻接收不到的情況,這種情況常常讓人很苦惱,然后就會(huì)去仔細(xì)對(duì)比前后端的參數(shù)單詞是不是對(duì)應(yīng)上了,也會(huì)去檢查是不是前端的請(qǐng)求參數(shù)格式有問(wèn)題,又或者是后端接口接收的參數(shù)格式有問(wèn)題,一通檢查對(duì)比下來(lái),發(fā)現(xiàn)都沒(méi)問(wèn)題。那究竟是為什么呢?那就繼續(xù)往下看吧。
問(wèn)題重現(xiàn)
控制層代碼:
@PostMapping(value = "/test") public void test(@RequestBody UserVO userVO) { System.out.println("用戶代碼:" + userVO.getUCode()); System.out.println("用戶名稱:" + userVO.getUName()); }
參數(shù)實(shí)體類:UserVO
@Data public class UserVO { /** * 用戶代碼 */ private Long uCode; /** * 用戶名稱 */ private String uName; }
用postman模擬前端調(diào)用:
控制臺(tái)預(yù)期打印結(jié)果:
用戶代碼:12345
用戶名稱:小明
控制臺(tái)實(shí)際打印結(jié)果:
解決方式
在實(shí)體類的屬性上方加@JsonProperty
注解,如下圖:
然后測(cè)試控制臺(tái)打印結(jié)果:
原因分析
首先我們先把實(shí)體類復(fù)原,并且加上一個(gè)新的屬性loginType
@Data public class UserVO { /** * 用戶代碼 */ private Long uCode; /** * 用戶名稱 */ private String uName; /** * 登錄類型 */ private String loginType; }
眼尖的同學(xué)可能會(huì)發(fā)現(xiàn)了,我新加的屬性loginType長(zhǎng)得是不是跟原來(lái)兩個(gè)屬性u(píng)Code和uName不太一樣,不一樣的點(diǎn)在于uCode和uName都是首字母小寫(xiě),第二個(gè)字母大寫(xiě)的單詞,而loginType則不然。但是它們?nèi)挤像劮迕ǖ囊?guī)范,對(duì)吧。這時(shí)候可以猜測(cè),難道是這個(gè)原因?qū)е碌模?/p>
在這里我們先來(lái)簡(jiǎn)單驗(yàn)證下uCode、uName、loginType
的情況
通過(guò)斷點(diǎn)發(fā)現(xiàn),uCode、uName
是空的,loginType
卻不是空的
然后我們將uCode、uName
分別改為userCode、userName
后再進(jìn)行測(cè)試
@Data public class UserVO { /** * 用戶代碼 */ private Long userCode; /** * 用戶名稱 */ private String userName; /** * 登錄類型 */ private String loginType; }
這個(gè)時(shí)候我們就可以得出結(jié)論,原因就是首字母小寫(xiě),第二個(gè)字母大寫(xiě)的單詞的屬性是有問(wèn)題的。
但是我們不禁要問(wèn),為啥呢?它這也符合駝峰命名法的規(guī)范啊。為什么它就有問(wèn)題呢?感興趣的同學(xué)可以接著往下看。
原理分析
首先我們要知道,在Spring中,前后端之間數(shù)據(jù)傳輸會(huì)涉及到數(shù)據(jù)的序列化和反序列化的操作,并且SpringBoot默認(rèn)是使用Jackson作為JSON數(shù)據(jù)格式處理的類庫(kù)。
序列化:按照指定的格式、順序等將實(shí)體類對(duì)象轉(zhuǎn)換為JSON對(duì)象;
反序列化:將JSON對(duì)象中的字符串、數(shù)字等,將其轉(zhuǎn)換為實(shí)體對(duì)象;
那么現(xiàn)在咱們就來(lái)斷點(diǎn)調(diào)試Jackson
的源碼來(lái)看看原因。為方便展示,我將實(shí)體類留下uName、loginType
兩個(gè)屬性
@Data public class UserVO { /** * 用戶名稱 */ private String uName; /** * 登錄類型 */ private String loginType; }
開(kāi)始調(diào)試:
Jackon主要是通過(guò)抽象類AbstractJackson2HttpMessageConverter
的readJavaType
方法將 HTTP 請(qǐng)求中的消息體轉(zhuǎn)換為對(duì)象,所以我們找到這部分代碼,對(duì)他進(jìn)行斷點(diǎn)調(diào)試:
然后逐步斷點(diǎn),在上圖的第192行和第195行,它會(huì)調(diào)用ObjectMapper.readValue
,然后斷點(diǎn)推進(jìn)到調(diào)用方法的核心地方ObjectMapper
的_readMapAndClose
方法
this._findRootDeserializer(ctxt, valueType);
的大概意思就是根據(jù)類型找到反序列化器,注意在這邊是先從緩存中取,取到了的話就直接返回了。如果沒(méi)到下一步斷點(diǎn),在這邊你可以清除一下緩存。
然后斷點(diǎn)繼續(xù)推進(jìn)到創(chuàng)建反序列化器的地方DeserializerCache._createDeserializer
如果你清除緩存或者重啟項(xiàng)目在調(diào)用時(shí)會(huì)直接進(jìn)入到這個(gè)創(chuàng)建反序列化器的地方,你直接在這個(gè)方法上打斷點(diǎn)就好了
找到上圖中第164行的代碼,BeanDescription
是類的描述的意思,所有的屬性都在這里被解析,然后我們斷點(diǎn)進(jìn)去看看。會(huì)進(jìn)入到POJOPropertiesCollector.collectAll
方法,就是字面意思,收集所有。方法邏輯詳見(jiàn)下圖:
執(zhí)行完this._addFields(props);
后props
加入了uName
和loginType
執(zhí)行完this._addMethods(props);
后發(fā)現(xiàn)props
竟然多了一個(gè)uname
在這里我們點(diǎn)開(kāi)屬性詳細(xì)去看,會(huì)發(fā)現(xiàn)
uName
的get和set為空,但是loginType
的是正常的,并且uname
這個(gè)不知道哪里跑出來(lái)的屬性的get和set也是不為空的。
再接著執(zhí)行this._removeUnwantedProperties(props);
移除不想要的屬性之后,會(huì)發(fā)現(xiàn)就剩下loginType
和uname
了,因?yàn)?code>uName沒(méi)有g(shù)et和set。為什么
然后props中目前存儲(chǔ)的就是loginType
和uname
現(xiàn)在我們就要弄明白為什么有g(shù)et/set的是uname而不是uName
首先,在這個(gè)例子中我使用的是@Data
注解,也就是使用的 Lombok,也就是說(shuō) getter 和 setter 是由 Lombok 生成的。使用注解的話會(huì)將get/set方法隱藏起來(lái),然后我們可以通過(guò)IDEA的Structure
來(lái)看,見(jiàn)下圖:
那么Jackson 到底是如何解析的,使得解析出來(lái)的是uname
,而不是uName
。它解析的具體代碼在com.fasterxml.jackson.databind.util.BeanUtil
類中的legacyManglePropertyName
方法中
從上圖為我們可以很明顯的看到,通過(guò)這個(gè)方法之后getLoginType
被解析成loginType
了。那我們?cè)賮?lái)看看uName
,見(jiàn)下圖:
從上圖斷點(diǎn)我們可以清晰的看見(jiàn)getUName
被解析成uname
了,按照我們正常的思維邏輯的話,loginType和uName都符合駝峰命名法的規(guī)范,那么uName對(duì)應(yīng)的get方法解析出來(lái)應(yīng)該是uName啊,為什么變成了uname呢?原因就在于這個(gè)
legacyManglePropertyName
方法的處理邏輯,它的邏輯大概是:
1.根據(jù)入?yún)ffset去除get或者get,然后就剩下UName或者LoginType了
2.然后從第一個(gè)字母開(kāi)始解析,如果第一個(gè)字母是大寫(xiě)的,于是就將它轉(zhuǎn)成小寫(xiě),然后找下一個(gè),如果還是大寫(xiě),就繼續(xù)轉(zhuǎn)成小寫(xiě),直到找到一個(gè)小寫(xiě)字母后,就把之后的字母(不管大小寫(xiě))一起拼接進(jìn)來(lái)。
這樣就能解釋了:
去除get之后的
LoginType
找到第一個(gè)字母是大寫(xiě),轉(zhuǎn)為小寫(xiě)的l
,下一個(gè)字母是小寫(xiě)的了,就直接把后面的全拼接進(jìn)來(lái),最終形成了loginType
去除get之后的
UName
找到第一個(gè)字母是大寫(xiě),轉(zhuǎn)為小寫(xiě)的u
,下一個(gè)字母又是大寫(xiě),轉(zhuǎn)為小寫(xiě)的n
,在下一個(gè)字母是小寫(xiě)的了,就直接把后面的全拼接進(jìn)來(lái),最終形成了uname
如果說(shuō)這邊的getUName
換成getuName
,那么解析出來(lái)的就是正確的uName
了。
結(jié)論
到這里,我們就可以得出結(jié)論了
因?yàn)?Lombok 生成 get、set 方法的語(yǔ)義規(guī)范與和Jackson 處理 get、set 方法之間的不一致,導(dǎo)致屬性名無(wú)法匹配上,最終也就導(dǎo)致了前端明明傳了參數(shù),后端卻接收不到的問(wèn)題。
擴(kuò)展
我后面去github的 lombok社區(qū) 了解了相關(guān)內(nèi)容,lombook社區(qū)是這樣描述的:
用網(wǎng)頁(yè)翻譯給他翻譯成中文,翻譯有些不對(duì),但是能看明白大概意思就行
lombok的大概意思就是:我就是這樣的規(guī)范,即使其他的工具框架都改了,我也不改,但是建議你們不要使用首字母小寫(xiě)第二個(gè)字母大寫(xiě)的屬性名,避免出現(xiàn)問(wèn)題,可能知名度比較高的框架都比較傲嬌吧哈哈。
但是lombok還是給出了一個(gè)解決方案,加上這個(gè)配置項(xiàng)
lombok.accessors.capitalization = [basic | beanspec] (default: basic)
其中basic代表遵循lombok的規(guī)范(getUName);beanspec代表遵循Spring、Jackson 的規(guī)范(getuName)。默認(rèn)是basic。
看到這里,我就來(lái)總結(jié)一下能解決這個(gè)問(wèn)題的三種方案吧
1. 加@JsonProperty注解強(qiáng)行指定屬性名
@Data public class UserVO { /** * 用戶名稱 */ @JsonProperty(value = "uName") private String uName; /** * 登錄類型 */ private String loginType; }
2.不使用lombok,使用IDEA默認(rèn)生成get/set方法
3.加上lombok配置項(xiàng)
lombok.accessors.capitalization = [basic | beanspec] (default: basic)
最后,博主的建議是,盡量不要用這種命名方式,如果非要用,那就加上@JsonProperty
注解強(qiáng)行指定屬性名,這樣比較方便。
總結(jié)
到此這篇關(guān)于spring中前端明明傳了值后端卻接收不到問(wèn)題解決辦法的文章就介紹到這了,更多相關(guān)spring前端傳值后端接收不到內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中使用同步回調(diào)和異步回調(diào)的示例詳解
這篇文章主要介紹了Java中使用同步回調(diào)和異步回調(diào)的相關(guān)資料,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-04-04spring boot實(shí)戰(zhàn)教程之shiro session過(guò)期時(shí)間詳解
這篇文章主要給大家介紹了關(guān)于spring boot實(shí)戰(zhàn)教程之shiro session過(guò)期時(shí)間的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-10-10Java基礎(chǔ)學(xué)習(xí)之構(gòu)造方法詳解
這篇文章主要為大家詳細(xì)介紹了Java基礎(chǔ)學(xué)習(xí)中構(gòu)造方法的概述及注意事項(xiàng),文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Java有一定幫助,需要的可以參考一下2022-08-08解決dubbo錯(cuò)誤ip及ip亂入問(wèn)題的方法
今天小編就為大家分享一篇關(guān)于解決dubbo錯(cuò)誤ip及ip亂入問(wèn)題的方法,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03Java事務(wù)管理學(xué)習(xí)之Spring和Hibernate詳解
這篇文章主要給大家介紹了Java事務(wù)管理學(xué)習(xí)之Spring和Hibernate的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友們可以參考借鑒,下面來(lái)一起看看吧。2017-03-03@SpringBootTest 注解報(bào)紅問(wèn)題及解決
這篇文章主要介紹了@SpringBootTest 注解報(bào)紅問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11