再也不敢精通Java了——get/set篇

2022-01-03 Java技術迷

點擊關注公眾號,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

相關焦點

  • java技能提升,用Lombok甩掉get和set,讓代碼變得更簡潔
    他不服氣的說:你來看嘛,就是有問題,Dao實體get()和set()方法都沒有。此處省略10000字,讓我流一會兒技術人的眼淚。Lombok通常我們代碼裡的實體Dao或者自定義Bean都會有get()和set()方法,set是設置的意思,而get是獲取的意思,顧名思義,這兩個方法是對數據進行設置和獲取用的。一般來說set和get方法都是對私有域變量進行操作的,所以大多數都是使用在包含特定屬性的類實體中。
  • Java代碼中寫set/get方法了,逮到罰款!
    你的 Java 代碼中還充斥著大量的 set/get 方法?我們在剛開始學習 Java 語言的時候講過,面向對象的三大特徵就是封裝,繼承,和多態。在 Java 中,要保證封裝性,需要將成員變量私有化,對外提供 set/get 方法來訪問,雖然現在的 IDE,像 eclipse,IDEA都提供了快捷鍵,來生成 set/get 方法,但是在做項目的時候,一個 JavaBean 往往會有很多的成員變量,一個變量對應兩個方法,如果有10幾個成員變量,那麼會對應20多個方法,也許還要去寫構造器、equals 等方法,而且需要維護。
  • Java Set集合的詳解
    如果對兩個引用調用hashCode方法,會得到相同的結果,如果對象所屬的類沒有覆蓋Object的hashCode方法的話,hashCode會返回每個對象特有的序號(java是依據對象的內存地址計算出的此序號),所以兩個不同的對象的hashCode值是不可能相等的。
  • 面試官:Java 有線程安全的 set 嗎?我竟然答不上來..
    java有沒有提供默認實現呢?在java的concurrent包中,我找到了CopyOnWriteArraySet,那麼它是線程安全的嗎?下面是測試代碼。);    int times = 10000;    AtomicInteger flag = new AtomicInteger(0);    for(int i = 0; i < times; i ++){        service.execute(()->{            set.add("a" + flag.getAndAdd
  • CTO:不要在 Java 代碼中寫 set/get 方法了,逮一次罰款500
    你的 Java 代碼中還充斥著大量的 set/get 方法?我們在剛開始學習 Java 語言的時候講過,面向對象的三大特徵就是封裝,繼承,和多態。在 Java 中,要保證封裝性,需要將成員變量私有化,對外提供 set/get 方法來訪問,雖然現在的 IDE,像 eclipse,IDEA都提供了快捷鍵,來生成 set/get 方法,但是在做項目的時候,一個 JavaBean 往往會有很多的成員變量,一個變量對應兩個方法,如果有10幾個成員變量,那麼會對應20多個方法,也許還要去寫構造器、equals 等方法,而且需要維護。
  • Java中的get()方法和set()方法
    在Java中,為了數據的安全,換句話說就是為了隱藏你的代碼的一些實現細節,我們會用private來修飾屬性,使用private修飾的屬性就不能被其他類直接訪問了,想要訪問就需要通過set
  • Java 反射,這篇寫的很透徹!
    <init>() at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.newInstance(Class.java:412) ... 2 more這是因為我們重寫了構造方法,而且是有參構造方法,如果不寫構造方法
  • Java基礎之反射篇
    > p = people.getClass();如果有人問起Class,這個 是什麼意思?,我只能說「年輕人,我勸你耗子尾汁,白嫖的Java基礎之泛型篇 你都不要?」。public boolean java.lang.Object.equals(java.lang.Object)public java.lang.String java.lang.Object.toString()public native int java.lang.Object.hashCode()public final native java.lang.Class
  • Python進階之hasattr()、getattr()和setattr()函數的使用
    A, 'age')False>>>>>> hasattr(A, 'func')True2. getattr(object, name[, default])獲取object對象的屬性的值,如果存在則返回屬性值,如果不存在分為兩種情況:(1)沒有default參數時,會直接報錯;
  • 優雅的寫一個JavaBean,免去生成get和set的終極解決方案
    開發中我們要寫大量的java實體類,雖然idea能夠直接生成get和set方法,有的時候碰到那種屬性很多的實體類,看代碼都看的頭痛。現在介紹一款好用的插件給各位小夥伴,它能夠讓代碼更簡潔好看,不需要生成get和set方法,編譯也能通過。
  • Java SPI一探究竟 - 第343篇
    Spring Boot中使用Mockito - 第338篇Spring Boot中使用Mockito進行Web測試 - 第339篇Mockito中捕獲mock對象方法的調用參數[SpringBoot]SpringBoot使用Mockito mock靜態方法/私有方法 - 第341篇SpringBoot使用Powermockito
  • (實用篇)淺談PHP攔截器之__set()與__get()的理解與使用方法
    以下是文章分享2群,由於群人數已超過300,不能掃碼進群,這個任務呢,就由小篇來拉你們進群了,掃描下面二維碼,加小篇好友~「一般來說,總是把類的屬性定義為private,這更符合現實的邏輯。但是,對屬性的讀取和賦值操作是非常頻繁的,因此在PHP5中,預定義了兩個函數「__get()」和「__set()」來獲取和賦值其屬性,以及檢查屬性的「__isset()」和刪除屬性的方法「__unset()」。
  • Java修飾符關鍵詞大全
    最近,有人問我關於Java修飾符關鍵字的一個問題,但我根本不知道那是什麼。所以我覺得除了實際編程和算法,我也有必要學習這些內容。通過谷歌搜索,我只得到一些瑣碎的要點,並不完整。所以我以此主題寫了這篇文章。這也是一個可用於測試你的計算機科學知識的面試問題。Java修飾符是你添加到變量、類和方法以改變其含義的關鍵詞。
  • java如何通過反射操作欄位
    我照樣要訪問》這三篇文章,描述了通過java反射創建對象以及調用方法,有興趣的朋友可以翻閱一下。今天我再來寫寫怎麼通過反射操作欄位吧。老規矩,先上我們要操作的類的代碼。這個demo類比較簡單,一個非靜態的欄位name,一個靜態的欄位staticName,都賦值了初始值。
  • Java基礎篇(04):日期與時間API用法詳解
    二、JDK原生API1、Date基礎基礎用法java.sql.Date繼承java.util.Date,相關方法也大部分直接調用父類方法。public class DateTime01 {    public static void main(String[] args) {        long nowTime = System.currentTimeMillis() ;        java.util.Date data01 = new java.util.Date(nowTime);
  • 關於 Java 對象序列化您不知道的 5 件事
    除非對每個持久化的用戶設置運行某種類型的數據轉換實用程序(極其龐大的任務),否則以後似乎只能一直用Hashtable 作為應用程式的存儲格式。團隊感到陷入僵局,但這只是因為他們不知道關於 Java 序列化的一個重要事實:Java 序列化允許隨著時間的推移而改變類型。當我向他們展示如何自動進行序列化替換後,他們終於按計劃完成了向 HashMap 的轉變。
  • 20個非常有用的Java程序片段
    內容比較早,有些函數可能過時了,但是總體思路是不錯滴,供參考。10、使用iText JAR生成PDF閱讀這篇文章 了解更多細節import java.awt.Dimension;  import java.awt.Rectangle;  import java.awt.Robot;  import java.awt.Toolkit;  import java.awt.image.BufferedImage;  import javax.imageio.ImageIO
  • Java反射是什麼?看這篇絕對會了!
    反射是java語言的一個特性,它允程序在運行時(注意不是編譯的時候)來進行自我檢查並且對內部的成員進行操作。例如它允許一個java的類獲取他所有的成員變量和方法並且顯示出來。Java 的這一能力在實際應用中也許用得不是很多,但是在其它的程序設計語言中根本就不存在這一特性。例如,Pascal、C 或者 C++ 中就沒有辦法在程序中獲得函數定義相關的信息。
  • 《手把手教你》系列技巧篇(七)-java+ selenium自動化測試-宏哥帶你全方位吊打Chrome啟動過程(詳細教程)
    1.簡介 經過前邊幾篇文章和宏哥一起的學習,想必你已經知道了如何去查看Selenium相關接口或者方法。
  • CTO:不要在代碼中寫 set/get 方法了,逮一次罰款...
    你的 Java 代碼中還充斥著大量的 set/get 方法於是公司出了規定:不要在代碼中寫 set/get 方法了,逮一次罰款。剛開始學習 Java 語言的時候,面向對象的三大特徵就是封裝,繼承,和多態。