點擊關注公眾號,Java乾貨及時送達
小夥伴們好呀,今天 4ye 來和大家分享在項目中遇到的一個特別有意思的 『bug』 😄請看~import lombok.Data;
@Data
public class UserDTO {
private String uName;
private boolean active;
private Boolean closed;
private Boolean isDeleted;
private boolean isActive2;
}上面的這個 DTO 中,生成的 get/set 方法是啥樣子的呢?(注意是 lombok 生成的)
image-20211216074356505比如
是 getIsDeleted 還是 isDeleted是 getIsActive2 還是 isActive2上面是 get 的情況,那 set 呢?
請思考下,接下來的答案可能會和你想的有點出入~
答案如下
lombok版是不是有點吃驚 哈哈
先來點簡單的~
Boolean這個就很簡單啦,生成的都是我們我們平時用到的樣子,過~
boolean這個 active 是基本數據類型的 boolean ,生成的 get 方法是 isActive , set 方法是 setActive ,很正常🐖
但是你會發現這個 boolean isActive2 很不一樣,它生成的 get 方法是 isActive2 , set 方法是 setActive2 。
按理來說應該生成 isIsActive2 方法和 setIsActive2 方法才對呀,結果居然沒有!
請問:你覺得這個是 lombok 的鍋還是 java 本身的設計 🐷
為了排除嫌疑,我用 idea 自動生成 get/set ,結果它倆居然是一樣的,那這個應該就是 java 的某種特點 了
不知道小夥伴們還記得 阿里的Java開發手冊 沒,裡面就提到了不要用這個 is 前綴去修飾 pojo 中的 boolean 變量。
不過應該也很少人在這個 pojo 中定義 boolean 類型了叭~ 這個也在 手冊中有提到 ,畢竟 null 也的話還能表示數據接受的異常等
String uName從上面可以發現,lombok 生成的是 getUName 和 setUName ,而如果通過 IDEA 去生成的話,是生成這個 getuName 和 setuName 。
請先記住這個點,下面正片開始~
如圖所示,這個就是折磨了我快一天的 bug,測試接口時,發現了這麼詭異的一幕,後端只定義了這個 tDate 屬性,壓根就沒有 tdate 這個屬性,可是前端 post 數據時,居然給我傳了這兩個參數上來,而且詭異的是,我後端還接受不到!
我當時就懵了,想著這前端寫的啥代碼,怎麼給我搞這齣…… 🐷
於是乎,我們愉快的進行了溝通~
結果發現,這個是在更新數據時出現的,而這個 tdate 屬性是我傳回來的,而且就是 null
我仔細看了下,發現這居然是真的,我的天,我後臺明明沒有這個 tdate 的!
於是乎,我開始了 扒源碼 之路 (就那種直接懟 很笨的做法😅)
直接從 tomcat 到這個 SpringMVC ,最後看到這個 Jackson 時才醒悟過來 (驚呼:我在幹什麼!🐖)
原理圖如圖 ,後端接收到 request 請求時,要將數據進行 反序列化,轉換成我們接口中使用的對象。
您猜怎麼著,這反序列化的過程,居然不是直接使用我們定義好的屬性欄位,而是通過 get/set 方法去推測出來的!!
這個過程比較複雜,先來看這個請求數據 👇
發出的請求這裡切入有點唐突~ 因為這個 debug 過程很長,我也記不住,就記住下面這些要點。🐖
請求過程請求時,會來到這麼一個方法,而在進入這個 _addMethods 方法時,這裡還是正常的五個屬性
image-20211219220118077進入之後,會調用到這個方法 legacyManglePropertyName ,最後會返回這個 uname 屬性名字(後面再解釋)
出來後,這個 props 直接變成下面 7 個了,包括這個 isActive2 直接變成 active2 屬性。
接下來的一步,就是執行上面的這個 _removeUnwantedProperties 方法,它會移除不想要的屬性。(指上面 _addFields 和 _addMethods 推測出來的屬性和方法中,所有 isVisible 值為 false 的會被移除掉 )
執行 _removeUnwantedAccessor 去移除 不需要的 get/set 方法執行這個 _renameProperties 方法。這個會根據我們使用的 註解 @JsonProperty("uName") 來重命名我們的這個屬性。執行到最後,會變成這樣子,方法名字還是 getUNAME/setUNAME , 但是我們這個屬性名字卻是 uname
關鍵點省略一大堆步驟……(怎麼提取請求中的body,並獲取其中的欄位,匹配到相應的請求參數中 等),直接來到關鍵點這個 反序列化的賦值操作 ,可以看到這裡會將我們的 json 請求中的欄位提取出來,然後進行匹配,找不到的話,就無法賦值。
這裡面還使用了這個 散列數組 _hashArea 來存儲這個屬性 。
這裡已經匹配不上了,所以這個我們的 DTO 中獲取不到值
效果如下 👇
響應過程這裡就涉及到這個序列化的過程了, 這個 debug 起來也比較簡單了 就不過的贅述啦~
反序列化時會執行到一個 serializeValue 方法 ,會執行到一個 serializeFields 方法 (將欄位進行序列化)
_props 對應的五個屬性如下 👇
很明顯這個 uname 就從這裡出現的,最後得到的結果就如下了 😅
解決辦法也很簡單,就是用 @JsonProperty("uName") 去定義好這個 屬性名稱就好了 。
思考到這裡,我們就簡單了解了這個 請求怎麼反序列化成為一個對象,以及對象怎麼序列化,對客戶端進行響應的一個過程。
同時我們也了解到 Jackson 有它自己的獲取屬性的規則,會將我們的 uName 變成這個 uname
參考上面的這個 legacyManglePropertyName 方法了 👇 (這個在 jackson-databind-2.12.4.jar 版本中,之前2.11的代碼是用到那個 BeanUtil 包下的,小夥伴們可以自己看看,不過現在標記為 過期的 了。)
那麼 ,lombok 怎麼生成這個 get 方法呢?
這裡參考下這篇文章 ,了解下 lombok 的工作原理
https://www.cnblogs.com/heyonggang/p/8638374.html
那個語法樹啥的我也沒有試過~,感覺不懂的地方又多了億點點
這道題太難了!我不會!不過根據文章給出的信息,我們知道 在 lombok 的源碼中有很多 Handle 專門來處理每一個 lombok 註解,如下(源碼直接在 github 上下載)
生成 get 方法解密 ,可以看到在源碼中,有個很顯眼的 toGetterName 方法,
它會去調用這個 toAccessorName 方法,可以看到這裡傳了一個 get 前綴字符串
最後會來到這個 buildAccessorName 方法,沒猜錯的話,這裡就是真正創建的方法了。
果然,可以看到如下代碼 ,capitalize 翻譯過來就是 把……首字母大寫 (那應該沒找錯了~)
最後,來到這個 CapitalizationStrategy 枚舉類中,發現默認用了這 BASIC ,把其中的方法拷貝出來運行下,就可以證實我們的猜測了
代碼如下
// BASIC
public String capitalize(String in) {
if (in.length() == 0) return in;
char first = in.charAt(0);
if (!Character.isLowerCase(first)) return in;
boolean useUpperCase = in.length() > 2 &&
(Character.isTitleCase(in.charAt(1)) || Character.isUpperCase(in.charAt(1)));
return (useUpperCase ? Character.toUpperCase(first) : Character.toTitleCase(first)) + in.substring(1);
}
// BEANSPEC
public String capitalize2(String in) {
if (in.length() == 0) return in;
char first = in.charAt(0);
if (!Character.isLowerCase(first) || (in.length() > 1 && Character.isUpperCase(in.charAt(1)))) return in;
boolean useUpperCase = in.length() > 2 && Character.isTitleCase(in.charAt(1));
return (useUpperCase ? Character.toUpperCase(first) : Character.toTitleCase(first)) + in.substring(1);
}
@Test
void testName(){
System.out.println(capitalize("tDate")); // TDATE
System.out.println(capitalize2("tDate")); // tdate
}
總結閱讀完後,希望你能記住以下幾點~
一. 屬性名稱一定不要弄成有歧義的那種,不然我們都猜不透這個 get/set 是什麼樣子的!比如 uName 這種第二個字母就大寫的!
二. 如果非要寫成 uName ,建議自己手寫 get/set 或者 使用 @JsonProperty 註解。
三. Jackson 是從get,set方法中推測屬性的
四. 使用到 Lombok 相關註解時,它會在編譯期根據自己的規則幫我們生成 get/set 方法。
擴展一. 在閱讀 Jackson 源碼時,發現它使用到這個 LRUMap ,會推測第一次請求到的對象屬性,並緩存到 props 中,最多存 2000 個。
二. Java 中有一個 Introspector 類,這個和 JavaBean 的規範有關 ,地址 https://www.oracle.com/java/technologies/javase/javabeans-spec.html
(我暈了 😵)
這個方法的作用是 使首字母變小 ,而且在 Spring 的這些包中使用到!貌似也是用來推測屬性,小夥伴們可以自行研究~
三. 一開始我以為是 bug,結果來到 Jackson 的 GitHub issue 地址 ,卻發現這個 19 年就有了 天吶,早知道我就直接搜 bug 好了,損失了一個 PR 和億點點時間 🐖,不過也是在這裡了解到上面那個 Introspector 的 😂 (好複雜)
https://github.com/FasterXML/jackson-databind/issues/2327