Lombok首字母小寫,第二個字母大寫,jackson反序列化失敗

軒與木修發表於2023-03-30

記一次介面呼叫欄位對映失敗問題排查

在寫介面的時候遇到一個很神奇的問題,編寫一個post介面,在使用包裝類接收body的時候發現有個欄位對映不上。程式碼如下

@RestController
public class TestController {
    @PostMapping("test")
    public TestDto test(@RequestBody TestDto testDto) {
        return testDto;
    }
}
..........
@Getter
@Setter
public class TestDto {
    private String sName;
    private String value;
}

TestDto中的value可以正常獲取值,但是sName卻沒值。
根據我多年的開發經驗,推測應該是欄位名的問題,大家都知道springboot介面反序列化是用的jackson,而jackson又是呼叫的getter和setter實現的序列化和反序列化,這樣我們們大概就有一個方向。直接看一下class檔案,看看@getter和@setter註解生成的方法長什麼樣

看起來感覺沒什麼問題。
那就只能debug看了,為了方便除錯,手動編寫getter和setter方法(複製過來),然後在setSName方法上打上斷點,結果根本沒停,說明根本就沒有呼叫該方法。那我們再試試在setValue方法上打上斷點,這次走到了

看一下方法呼叫,發現前幾個都是invoke,沒啥用,往前看幾個,發現deserializeAndSet方法,沒看到什麼有用的資訊

再往前看一個,看看deserializeFromObject方法裡面,發現這麼一個判斷

看一下_beanProperties

發現這裡就是儲存類欄位的資訊地方,但是,嗯?為什麼是sname,難怪我們的sName無法對映上。
那我們接下來就找找看,這個_beanProperties到底是怎麼來的

在_beanProperties上打上斷點,調一下介面,嗯?怎麼沒停?難不成這個是系統啟動的時候就載入好的,重啟一下試試。還是沒有!再調一下介面試試,這次終於是停了

我們再來看看這properties是哪來的

繼續深入

終於看到了我們熟悉的內容,這不就是我們要找的東西嗎,發現又是從props來的,再回頭

繼續在_properties上打上斷點,注意一定要把Field access勾選上,不然沒辦法監聽到引數的使用

重啟服務,呼叫介面,然後進入addProperty方法

然後繼續反推,發現propDefs

閱讀方法後發現資料來源於beanDesc.findProperties(),深入得到

繼續在這個新的_properties上打上斷點,重啟服務,跳過幾個無用斷點,除錯介面

深入方法,發現collectAll方法

看到_addFields和_addMethods方法,感覺終於要接近真相了!
接下來我們就看看collectAll方法到底做了什麼

可以看到,在_addFields之後,props裡面已經存放了TestDto的兩個欄位,這個時候sName的key還是對的

進入_addMethods方法,這段程式碼的邏輯是遍歷類裡的所有方法,如果方法的入參是0個就嘗試從add getter方法,如果方法入參是1個則嘗試add setter方法。
進入_addGetterMethod方法,發現如下邏輯,如果方法上沒有新增json註解就會嘗試從方法名稱中解析欄位名

進入findNameForRegularGetter方法,發現最終是呼叫的legacyManglePropertyName解析欄位名

進入該方法,終於真相大白,重點看下面這段邏輯

這個方法中和方法名一起傳進來的還有getter字元的offset用於去除get字首,然後得到方法中的屬性名,首先將屬性名的第一個字元變為小寫,如果本來就是小寫的話直接返回屬性名,如getvalue->value。
如果不是則繼續遍歷剩餘字元,將每一個遇到的大寫字元都轉化為小寫,直到遇到第一個小寫的字元,然後返回屬性名。如getVAlue->value,
getSName->sname,getURL->url。本來_addMethods方法是為了給每個field新增對應的getter和setter方法的,但是現在由於從getter方法中解析的名稱和真實的field不一致,就導致會新增一個該名稱的欄位,我們的sname就是這麼來的

這個時候props裡面實際上有三個欄位,而sName裡面是沒有getter,setter方法的,我們的getSName方法被sname欄位拿去了。然後在經過_removeUnWantedProperties(props)方法之後,沒有getter和setter方法的欄位就被移除了!

結論

jackson反序列化的邏輯是,先找到類的成員變數field,然後從getter setter方法中反推屬性名,為field和方法新增對映,而jackson反推屬性名的邏輯是方法中去掉get的部分後跟著的連續大寫字元都轉換為小寫字元。

寫在最後

實際上這也和lombok生成getter方法的邏輯有關,如果我用idea自動生成getter方法的話,這個邏輯和jackson是一致的

相關文章