lombok?子類(lèi)中如何使用@Builder問(wèn)題
lombok子類(lèi)中如何使用@Builder
lombok大家都知道,在使用POJO過(guò)程中,它給我們帶來(lái)了很多便利,省下大量寫(xiě)get、set方法、構(gòu)造器、equal、toString方法的時(shí)間。除此之外,通過(guò)@Builder注解,lombok還可以方便的時(shí)間建造者模式。
但是,在使用@Builder過(guò)程中,我發(fā)現(xiàn)了一問(wèn)題:子類(lèi)的Builder對(duì)象沒(méi)有父類(lèi)的屬性。這在使用上造成了一定的問(wèn)題。
幾番搜索,對(duì)于這個(gè)問(wèn)題,找到了如下解法,解法的鏈接會(huì)放到文末。
1. 對(duì)于父類(lèi),使用@AllArgsConstructor注解
2. 對(duì)于子類(lèi),手動(dòng)編寫(xiě)全參數(shù)構(gòu)造器,內(nèi)部調(diào)用父類(lèi)全參數(shù)構(gòu)造器,在子類(lèi)全參數(shù)構(gòu)造器上使用@Builder注解
通過(guò)這種方式,子類(lèi)Builder對(duì)象可以使用父類(lèi)的所有私有屬性。
但是這種解法也有兩個(gè)副作用:
1. 因?yàn)槭褂肁llArgsConstructor注解,父類(lèi)構(gòu)造函數(shù)字段的順序由聲明字段的順序決定,如果子類(lèi)構(gòu)造函數(shù)傳參的時(shí)候順序不一致,字段類(lèi)型還一樣的話(huà),出了錯(cuò)不好發(fā)現(xiàn)
2. 如果父類(lèi)字段有增減,所有子類(lèi)的構(gòu)造器都要修改
雖然有這兩個(gè)副作用,但是這種解法是我找到的唯一一種解決子類(lèi)使用@Builder,能使用父類(lèi)屬性的方式。
參考博客:
Lombok’s @Builder annotation and inheritance
副作用見(jiàn)博客評(píng)論
另,這個(gè)博主對(duì)lombok使用很有心得,我閑看還看到他另一篇涉及到@Builder的文章,將如何在使用@Builder的模式中,加入字段的默認(rèn)值。因?yàn)槭褂昧私ㄔ煺吣J剑敲匆话阍陬?lèi)內(nèi)聲明字段的時(shí)候給字段默認(rèn)值的方式就是無(wú)效的,需要在建造者上動(dòng)手腳。
方式是:
1. 自定義靜態(tài)內(nèi)部類(lèi)作為建造者,賦予默認(rèn)值,再使用@Builder注解,這個(gè)時(shí)候lombok會(huì)補(bǔ)全已有的建造者類(lèi),進(jìn)而使用默認(rèn)值
2. 更新的lombok有@Builder.Default聲明,注解在需要默認(rèn)值的字段上即可。
在評(píng)論區(qū)也有這種方式的副作用討論,可以一看。鏈接是:
Using Lombok’s @Builder annotation with default values
子類(lèi)使用lombok的@Builder注解正確姿勢(shì)
在實(shí)際開(kāi)發(fā)中,有時(shí)候需要對(duì)子類(lèi)使用lombok的 @Builder注解來(lái)使用builder模式構(gòu)造該子類(lèi)對(duì)象。
父類(lèi)
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class Parent { private Long id; private String name; }
子類(lèi)
import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @Builder public class Child extends Parent{ }
此時(shí)雖然在子類(lèi)上添加了@Builder注解,但是由于子類(lèi)沒(méi)有屬性,如下圖所示,無(wú)法使用builder模式。
分析一下
通過(guò)閱讀 lombok.Builder的源碼,可知 @Builder 注解不僅可以用在類(lèi)上,還可以用在構(gòu)造函數(shù)上。
因此嘗試如下寫(xiě)法:
@EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @Builder public class Child extends Parent { @Builder private Child(Long id, String name) { super(id, name); } }
再次運(yùn)行上面的單元測(cè)試,發(fā)現(xiàn)支持了 builder 模式,但是奇怪的是,單測(cè)不通過(guò)。
java.lang.AssertionError:
Expected :1024
Actual :null
因此我們觀察一下 Child.class 反編譯后的代碼:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.chujianyun.libs.lombok; public class Child extends Parent { private Child(Long id, String name) { super(id, name); } public static Child.ChildBuilder builder() { return new Child.ChildBuilder(); } public boolean equals(final Object o) { if (o == this) { return true; } else if (!(o instanceof Child)) { return false; } else { Child other = (Child)o; if (!other.canEqual(this)) { return false; } else { return super.equals(o); } } } protected boolean canEqual(final Object other) { return other instanceof Child; } public int hashCode() { int result = super.hashCode(); return result; } public String toString() { return "Child()"; } public Child() { } public static class ChildBuilder { private Long id; private String name; ChildBuilder() { } public Child build() { return new Child(); } public String toString() { return "Child.ChildBuilder()"; } public Child.ChildBuilder id(final Long id) { this.id = id; return this; } public Child.ChildBuilder name(final String name) { this.name = name; return this; } } }
找到了原因,同時(shí)在子類(lèi)和全參數(shù)的構(gòu)造函數(shù)使用 @Builder 注解,會(huì)有 BUG,即最終的 build() 函數(shù)只是返回了空參的構(gòu)造函數(shù)創(chuàng)建了一個(gè) Child 對(duì)象,因此屬性“采用 builder 方式設(shè)置的 id 和 name” 最終“丟失”。
那么如何解決這個(gè)問(wèn)題呢?
我們?cè)俅位氐紷Builder 源碼的注釋上:
If a member is annotated, it must be either a constructor or a method. If a class is annotated,* then a private constructor is generated with all fields as arguments* (as if {@code @AllArgsConstructor(access = AccessLevel.PRIVATE)} is present* on the class), and it is as if this constructor has been annotated with {@code @Builder} instead.
可知,將其加到類(lèi)上,相當(dāng)于包含所有屬性的私有構(gòu)造方法,且構(gòu)造方法上加上 @Builder 注解。
因此我們寫(xiě)的代碼可能有沖突,我們修改如下:
import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor public class Child extends Parent { @Builder private Child(Long id, String name) { super(id, name); } }
最終單測(cè)通過(guò)
我們觀察一下此時(shí)編譯后的代碼:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.chujianyun.libs.lombok; public class Child extends Parent { private Child(Long id, String name) { super(id, name); } public static Child.ChildBuilder builder() { return new Child.ChildBuilder(); } public boolean equals(final Object o) { if (o == this) { return true; } else if (!(o instanceof Child)) { return false; } else { Child other = (Child)o; if (!other.canEqual(this)) { return false; } else { return super.equals(o); } } } protected boolean canEqual(final Object other) { return other instanceof Child; } public int hashCode() { int result = super.hashCode(); return result; } public String toString() { return "Child()"; } public Child() { } public static class ChildBuilder { private Long id; private String name; ChildBuilder() { } public Child.ChildBuilder id(final Long id) { this.id = id; return this; } public Child.ChildBuilder name(final String name) { this.name = name; return this; } public Child build() { return new Child(this.id, this.name); } public String toString() { return "Child.ChildBuilder(id=" + this.id + ", name=" + this.name + ")"; } } }
此時(shí)的build() 函數(shù)才是我們需要的狀態(tài)。
從編譯后的代碼我們可以清晰地看出 lombok 通過(guò)@Builder 實(shí)現(xiàn)的 builder模式的核心邏輯。
即構(gòu)造內(nèi)部類(lèi),在內(nèi)部類(lèi)賦值屬性,build時(shí)調(diào)用含有所有屬性的構(gòu)造方法創(chuàng)建對(duì)象。
更多細(xì)節(jié)可以仔細(xì)查看 @Builder 注解的源碼和注釋?zhuān)榭垂俜降氖謨?cè) https://projectlombok.org/features/Builder
總結(jié):
遇到詭異的問(wèn)題一定不要輕易放過(guò)。分析問(wèn)題要有步驟,比如可以看源碼中是否有說(shuō)明,也可以看編譯后的代碼,還可以通過(guò)反匯編等,觀察注解對(duì)類(lèi)文件作出了哪些影響。還可以去看官方手冊(cè)。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot整合canal實(shí)現(xiàn)數(shù)據(jù)緩存一致性解決方案
canal主要用途是基于?MySQL?數(shù)據(jù)庫(kù)增量日志解析,提供增量數(shù)據(jù)訂閱和消費(fèi),canal是借助于MySQL主從復(fù)制原理實(shí)現(xiàn),本文將給大家介紹SpringBoot整合canal實(shí)現(xiàn)數(shù)據(jù)緩存一致性解決方案,需要的朋友可以參考下2024-03-03java遞歸實(shí)現(xiàn)樹(shù)形結(jié)構(gòu)數(shù)據(jù)完整案例
遞歸算法的代碼比較簡(jiǎn)潔,可讀性較好;但是在實(shí)際的業(yè)務(wù)處理中會(huì)出現(xiàn)多次的重復(fù)調(diào)用,如果處理不好,很容易出現(xiàn)StackOverflowError報(bào)錯(cuò),這篇文章主要給大家介紹了關(guān)于java遞歸實(shí)現(xiàn)樹(shù)形結(jié)構(gòu)數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2023-04-04java工具類(lèi)SendEmailUtil實(shí)現(xiàn)發(fā)送郵件
這篇文章主要為大家詳細(xì)介紹了java工具類(lèi)SendEmailUtil實(shí)現(xiàn)發(fā)送郵件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02Java并發(fā)編程變量可見(jiàn)性避免指令重排使用詳解
這篇文章主要為大家介紹了Java并發(fā)編程變量可見(jiàn)性避免指令重排使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Java 中Timer和TimerTask 定時(shí)器和定時(shí)任務(wù)使用的例子
這篇文章主要介紹了Java 中Timer和TimerTask 定時(shí)器和定時(shí)任務(wù)使用的例子,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-05-05