@Builder不好用,試試@SuperBuilder
相信 Lombok 插件大家一定不會陌生,一個常用的注解是:@Builer, 它可以幫我們快速實現(xiàn)一個builder模式。以常見的商品模型為例:
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public?class?ItemDTO?{
????/**
?????*?商品ID
?????*/
????private?Long?itemId;
????/**
?????*?商品標題
?????*/
????private?String?itemTitle;
????/**
?????*?商品原價,單位是分
?????*/
????private?Long?price;
????/**
?????*?商品優(yōu)惠價,單位是分
?????*/
????private?Long?promotionPrice;
}
一行代碼就可以構造出一個新的商品:
ItemDTO?itemDTO?=?ItemDTO.builder()
????????.itemId(6542744309L)
????????.itemTitle("測試請不要拍小番茄500g/盒")
????????.price(500L)
????????.promotionPrice(325L)
????????.build();
System.out.println(itemDTO);
這樣寫不但美觀,而且還會省去好多無用的代碼。
Builder注解的使用限制
當我們的實體對象有繼承的設計的時候,Builder注解就沒那么好用了,還是以商品實體為例,如果現(xiàn)在商品類都繼承自一個BaseDTO
@Builder
@NoArgsConstructor
public?class?BaseDTO?{
????/**
?????*?業(yè)務身份
?????*/
????private?String?bizType;
????/**
?????*?場景
?????*/
????private?String?scene;
}
這時候我們再使用Builder注解就會發(fā)現(xiàn),在子類中無法通過builder方法構造父類中的成員變量

給BaseDTO上加上Builder注解也不會有任何效果。事實上,Builder注解只管承接注解的這個類,而不會管他的父類或者子類。如果真的是這樣的話,遇到有繼承的類,只好又打回原形,寫一堆的setter方法了。
試試SuperBuilder吧
這個問題在lombokv1.18.2 版本之前其實很難辦,但是在這個版本官方引入了一個新的注解@SuperBuilder,無法build父類的問題迎刃而解
The
@SuperBuilderannotation produces complex builder APIs for your classes. In contrast to@Builder,@SuperBuilderalso works with fields from superclasses. However, it only works for types. Most importantly, it requires that all superclasses also have the@SuperBuilderannotation.
按照官方文檔的說法,為了能夠使用build方法,只需要在子類和父類上都加@SuperBuilder注解,我們試一下

果然現(xiàn)在就可以在子類的實例中 build 父類的成員變量了
Lombok的原理
Lombok自動生成代碼的實現(xiàn)也是依賴于 JVM 開放的擴展點,使其可以在編譯的時候修改抽象語法樹,從而影響最終生成的字節(jié)碼

圖片來源地址:http://notatube.blogspot.com/2010/12/project-lombok-creating-custom.html
為什么Builder不能處理父類的成員變量
我們可以翻一下Lombok的源碼,Lombok對所有的注解都有兩套實現(xiàn),javac和eclipse,由于我們的運行環(huán)境是Idea所以我們選擇javac的實現(xiàn),javac版本的實現(xiàn)在lombok.javac.handlers.HandleBuilder#handle這個方法中
JavacNode?parent?=?annotationNode.up();
if?(parent.get()?instanceof?JCClassDecl)?{
???job.parentType?=?parent;
???JCClassDecl?td?=?(JCClassDecl)?parent.get();
???ListBuffer?allFields?=?new?ListBuffer();
???boolean?valuePresent?=?(hasAnnotation(lombok.Value.class,?parent)?||?hasAnnotation("lombok.experimental.Value",?parent));
??//?取出所有的成員變量
???for?(JavacNode?fieldNode?:?HandleConstructor.findAllFields(parent,?true))?{
??????JCVariableDecl?fd?=?(JCVariableDecl)?fieldNode.get();
??????JavacNode?isDefault?=?findAnnotation(Builder.Default.class,?fieldNode,?false);
??????boolean?isFinal?=?(fd.mods.flags?&?Flags.FINAL)?!=?0?||?(valuePresent?&&?!hasAnnotation(NonFinal.class,?fieldNode));
??????//?巴拉巴拉,省略掉
}
這里的annotationNode就是Builder注解,站在抽象語法樹的角度,調(diào)用up方法得到的就是被注解修飾的類,也就是需要生成builder方法的類。
通過查看源代碼,@Builder注解是可以修飾類,構造函數(shù)和方法的,為了簡單起見,上面的代碼只截取了@Builder修飾類這一種情況,這段代碼關鍵的地方就在于調(diào)用HandleConstructor.findAllFields方法獲得類中所有的成員變量:
public?static?List?findAllFields(JavacNode?typeNode,?boolean?evenFinalInitialized)? {
???ListBuffer?fields?=?new?ListBuffer();
??//?從抽象語法樹出發(fā),遍歷類的所有的成員變量
???for?(JavacNode?child?:?typeNode.down())?{
??????if?(child.getKind()?!=?Kind.FIELD)?continue;
??????JCVariableDecl?fieldDecl?=?(JCVariableDecl)?child.get();
??????//Skip?fields?that?start?with?$
??????if?(fieldDecl.name.toString().startsWith("$"))?continue;
??????long?fieldFlags?=?fieldDecl.mods.flags;
??????//Skip?static?fields.
??????if?((fieldFlags?&?Flags.STATIC)?!=?0)?continue;
??????//Skip?initialized?final?fields
??????boolean?isFinal?=?(fieldFlags?&?Flags.FINAL)?!=?0;
??????if?(evenFinalInitialized?||?!isFinal?||?fieldDecl.init?==?null)?fields.append(child);
???}
???return?fields.toList();
}
這段代碼比較簡單,就是對類中的成員變量做了過濾,比如說,靜態(tài)變量就不能被@Builder方法構造。有一個有意思的點,盡管$可以合法的出現(xiàn)在java的變量命名中,但是Lombok對這種變量做了過濾,因此變量名以$開始的也不能被@Builder構造,經(jīng)過我們的驗證確實是這樣的。
如果我們用JDT AstView看一下ItemDTO的抽象語法樹結構,發(fā)現(xiàn)Java的抽象語法樹設計的確是每個類只包含顯式聲明的變量而不包括父類的成員變量(該插件支持點擊語法樹節(jié)點可以和源文件聯(lián)動,且數(shù)量只有 4 個和ItemDTO聲明的成員變量數(shù)量一致)

因為findAllFields方法是從當前類的抽象語法樹出發(fā)去找所有的成員變量,所以就只能找到當前類的成員變量,而訪問不到父類的成員變量
一個鏡像的問題就是,既然@Builder注解不能構造父類的成員變量,那@SuperBuilder是怎么做到的呢?翻一下@SuperBuilder的源碼,核心邏輯在lombok.javac.handlers.HandleSuperBuilder#handle
//?巴拉巴拉省略
JCClassDecl?td?=?(JCClassDecl)?parent.get();
//?獲取繼承的父類的抽象語法樹
JCTree?extendsClause?=?Javac.getExtendsClause(td);
JCExpression?superclassBuilderClass?=?null;
if?(extendsClause?instanceof?JCTypeApply)?{
???//?Remember?the?type?arguments,?because?we?need?them?for?the?extends?clause?of?our?abstract?builder?class.
???superclassTypeParams?=?((JCTypeApply)?extendsClause).getTypeArguments();
???//?A?class?name?with?a?generics?type,?e.g.,?"Superclass".
???extendsClause?=?((JCTypeApply)?extendsClause).getType();
}
if?(extendsClause?instanceof?JCFieldAccess)?{
???Name?superclassName?=?((JCFieldAccess)?extendsClause).getIdentifier();
???String?superclassBuilderClassName?=?superclassName.toString()?+?"Builder";
???superclassBuilderClass?=?parent.getTreeMaker().Select((JCFieldAccess)?extendsClause,?parent.toName(superclassBuilderClassName));
}?else?if?(extendsClause?!=?null)?{
???String?superclassBuilderClassName?=?extendsClause.toString()?+?"Builder";
???superclassBuilderClass?=?chainDots(parent,?extendsClause.toString(),?superclassBuilderClassName);
}
//?巴拉巴拉省略
可以看到,這里拿到了繼承的父類的抽象語法樹,并在后面的邏輯中進行了處理,這里不再贅述
推薦??:?Github掘金計劃:Github上的一些優(yōu)質(zhì)項目搜羅
我是Guide哥,Java后端開發(fā),擁抱開源,喜歡烹飪,自由的少年。一個喜歡使用Lombok的技術人。我們下期再見
