spring中前端明明傳了值后端卻接收不到問題解決辦法
問題場景
在進行前后端的聯(lián)調(diào)時,有時候會出現(xiàn),前端明明傳了值,后端接口卻接收不到的情況,這種情況常常讓人很苦惱,然后就會去仔細對比前后端的參數(shù)單詞是不是對應上了,也會去檢查是不是前端的請求參數(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ù)實體類:UserVO
@Data
public class UserVO {
/**
* 用戶代碼
*/
private Long uCode;
/**
* 用戶名稱
*/
private String uName;
}
用postman模擬前端調(diào)用:

控制臺預期打印結(jié)果:
用戶代碼:12345
用戶名稱:小明
控制臺實際打印結(jié)果:

解決方式
在實體類的屬性上方加@JsonProperty注解,如下圖:

然后測試控制臺打印結(jié)果:

原因分析
首先我們先把實體類復原,并且加上一個新的屬性loginType
@Data
public class UserVO {
/**
* 用戶代碼
*/
private Long uCode;
/**
* 用戶名稱
*/
private String uName;
/**
* 登錄類型
*/
private String loginType;
}
眼尖的同學可能會發(fā)現(xiàn)了,我新加的屬性loginType長得是不是跟原來兩個屬性uCode和uName不太一樣,不一樣的點在于uCode和uName都是首字母小寫,第二個字母大寫的單詞,而loginType則不然。但是它們?nèi)挤像劮迕ǖ囊?guī)范,對吧。這時候可以猜測,難道是這個原因?qū)е碌模?/p>
在這里我們先來簡單驗證下uCode、uName、loginType的情況


通過斷點發(fā)現(xiàn),uCode、uName是空的,loginType卻不是空的
然后我們將uCode、uName分別改為userCode、userName后再進行測試
@Data
public class UserVO {
/**
* 用戶代碼
*/
private Long userCode;
/**
* 用戶名稱
*/
private String userName;
/**
* 登錄類型
*/
private String loginType;
}

這個時候我們就可以得出結(jié)論,原因就是首字母小寫,第二個字母大寫的單詞的屬性是有問題的。
但是我們不禁要問,為啥呢?它這也符合駝峰命名法的規(guī)范啊。為什么它就有問題呢?感興趣的同學可以接著往下看。
原理分析
首先我們要知道,在Spring中,前后端之間數(shù)據(jù)傳輸會涉及到數(shù)據(jù)的序列化和反序列化的操作,并且SpringBoot默認是使用Jackson作為JSON數(shù)據(jù)格式處理的類庫。
序列化:按照指定的格式、順序等將實體類對象轉(zhuǎn)換為JSON對象;
反序列化:將JSON對象中的字符串、數(shù)字等,將其轉(zhuǎn)換為實體對象;
那么現(xiàn)在咱們就來斷點調(diào)試Jackson的源碼來看看原因。為方便展示,我將實體類留下uName、loginType兩個屬性
@Data
public class UserVO {
/**
* 用戶名稱
*/
private String uName;
/**
* 登錄類型
*/
private String loginType;
}開始調(diào)試:
Jackon主要是通過抽象類AbstractJackson2HttpMessageConverter的readJavaType方法將 HTTP 請求中的消息體轉(zhuǎn)換為對象,所以我們找到這部分代碼,對他進行斷點調(diào)試:

然后逐步斷點,在上圖的第192行和第195行,它會調(diào)用ObjectMapper.readValue,然后斷點推進到調(diào)用方法的核心地方ObjectMapper的_readMapAndClose方法

this._findRootDeserializer(ctxt, valueType);的大概意思就是根據(jù)類型找到反序列化器,注意在這邊是先從緩存中取,取到了的話就直接返回了。如果沒到下一步斷點,在這邊你可以清除一下緩存。

然后斷點繼續(xù)推進到創(chuàng)建反序列化器的地方DeserializerCache._createDeserializer
如果你清除緩存或者重啟項目在調(diào)用時會直接進入到這個創(chuàng)建反序列化器的地方,你直接在這個方法上打斷點就好了

找到上圖中第164行的代碼,BeanDescription是類的描述的意思,所有的屬性都在這里被解析,然后我們斷點進去看看。會進入到POJOPropertiesCollector.collectAll方法,就是字面意思,收集所有。方法邏輯詳見下圖:

執(zhí)行完this._addFields(props);后props加入了uName和loginType

執(zhí)行完this._addMethods(props);后發(fā)現(xiàn)props竟然多了一個uname

在這里我們點開屬性詳細去看,會發(fā)現(xiàn)
uName的get和set為空,但是loginType的是正常的,并且uname這個不知道哪里跑出來的屬性的get和set也是不為空的。


再接著執(zhí)行this._removeUnwantedProperties(props);移除不想要的屬性之后,會發(fā)現(xiàn)就剩下loginType和uname了,因為uName沒有get和set。為什么

然后props中目前存儲的就是loginType和uname
現(xiàn)在我們就要弄明白為什么有get/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,見下圖:

從上圖斷點我們可以清晰的看見getUName被解析成uname了,按照我們正常的思維邏輯的話,loginType和uName都符合駝峰命名法的規(guī)范,那么uName對應的get方法解析出來應該是uName啊,為什么變成了uname呢?原因就在于這個
legacyManglePropertyName方法的處理邏輯,它的邏輯大概是:
1.根據(jù)入?yún)ffset去除get或者get,然后就剩下UName或者LoginType了
2.然后從第一個字母開始解析,如果第一個字母是大寫的,于是就將它轉(zhuǎn)成小寫,然后找下一個,如果還是大寫,就繼續(xù)轉(zhuǎn)成小寫,直到找到一個小寫字母后,就把之后的字母(不管大小寫)一起拼接進來。
這樣就能解釋了:
去除get之后的
LoginType找到第一個字母是大寫,轉(zhuǎn)為小寫的l,下一個字母是小寫的了,就直接把后面的全拼接進來,最終形成了loginType
去除get之后的
UName找到第一個字母是大寫,轉(zhuǎn)為小寫的u,下一個字母又是大寫,轉(zhuǎn)為小寫的n,在下一個字母是小寫的了,就直接把后面的全拼接進來,最終形成了uname
如果說這邊的getUName換成getuName,那么解析出來的就是正確的uName了。
結(jié)論
到這里,我們就可以得出結(jié)論了
因為 Lombok 生成 get、set 方法的語義規(guī)范與和Jackson 處理 get、set 方法之間的不一致,導致屬性名無法匹配上,最終也就導致了前端明明傳了參數(shù),后端卻接收不到的問題。
擴展
我后面去github的 lombok社區(qū) 了解了相關內(nèi)容,lombook社區(qū)是這樣描述的:

用網(wǎng)頁翻譯給他翻譯成中文,翻譯有些不對,但是能看明白大概意思就行

lombok的大概意思就是:我就是這樣的規(guī)范,即使其他的工具框架都改了,我也不改,但是建議你們不要使用首字母小寫第二個字母大寫的屬性名,避免出現(xiàn)問題,可能知名度比較高的框架都比較傲嬌吧哈哈。
但是lombok還是給出了一個解決方案,加上這個配置項
lombok.accessors.capitalization = [basic | beanspec] (default: basic)
其中basic代表遵循lombok的規(guī)范(getUName);beanspec代表遵循Spring、Jackson 的規(guī)范(getuName)。默認是basic。
看到這里,我就來總結(jié)一下能解決這個問題的三種方案吧
1. 加@JsonProperty注解強行指定屬性名
@Data
public class UserVO {
/**
* 用戶名稱
*/
@JsonProperty(value = "uName")
private String uName;
/**
* 登錄類型
*/
private String loginType;
}2.不使用lombok,使用IDEA默認生成get/set方法




3.加上lombok配置項
lombok.accessors.capitalization = [basic | beanspec] (default: basic)
最后,博主的建議是,盡量不要用這種命名方式,如果非要用,那就加上@JsonProperty注解強行指定屬性名,這樣比較方便。
總結(jié)
到此這篇關于spring中前端明明傳了值后端卻接收不到問題解決辦法的文章就介紹到這了,更多相關spring前端傳值后端接收不到內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java中使用同步回調(diào)和異步回調(diào)的示例詳解
這篇文章主要介紹了Java中使用同步回調(diào)和異步回調(diào)的相關資料,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04
spring boot實戰(zhàn)教程之shiro session過期時間詳解
這篇文章主要給大家介紹了關于spring boot實戰(zhàn)教程之shiro session過期時間的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起看看吧。2017-10-10

