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