以下文章來源於苦味代碼 ,作者L
相信Lombok插件大家一定不會陌生,一個常用的註解是:@Builer, 它可以幫我們快速實現一個builder模式。以常見的商品模型為例:
@Builder@AllArgsConstructor@NoArgsConstructor@Datapublic class ItemDTO { /** * 商品ID */ private Long itemId; /** * 商品標題 */ private String itemTitle; /** * 商品原價,單位是分 */ private Long price; /** * 商品優惠價,單位是分 */ private Long promotionPrice;}一行代碼就可以構造出一個新的商品:
ItemDTO itemDTO = ItemDTO.builder() .itemId(6542744309L) .itemTitle("測試請不要拍小番茄500g/盒") .price(500L) .promotionPrice(325L) .build();System.out.println(itemDTO);這樣寫不但美觀,而且還會省去好多無用的代碼。
Builder註解的使用限制
當我們的實體對象有繼承的設計的時候,Builder註解就沒那麼好用了,還是以商品實體為例,如果現在商品類都繼承自一個BaseDTO
@Builder@NoArgsConstructorpublic class BaseDTO { /** * 業務身份 */ private String bizType; /** * 場景 */ private String scene;}這時候我們再使用Builder註解就會發現,在子類中無法通過builder方法構造父類中的成員變量
給BaseDTO上加上Builder註解也不會有任何效果。事實上,Builder註解只管承接註解的這個類,而不會管他的父類或者子類。如果真的是這樣的話,遇到有繼承的類,只好又打回原形,寫一堆的setter方法了。
試試SuperBuilder吧
這個問題在lombokv1.18.2版本之前其實很難辦,但是在這個版本官方引入了一個新的註解@SuperBuilder,無法build父類的問題迎刃而解
The @SuperBuilder annotation produces complex builder APIs for your classes. In contrast to @Builder, @SuperBuilder also works with fields from superclasses. However, it only works for types. Most importantly, it requires that all superclasses also have the@SuperBuilder annotation.
按照官方文檔的說法,為了能夠使用build方法,只需要在子類和父類上都加@SuperBuilder註解,我們試一下
果然現在就可以在子類的實例中build`父類的成員變量了
Lombok的原理
Lombok自動生成代碼的實現也是依賴於JVM開放的擴展點,使其可以在編譯的時候修改抽象語法樹,從而影響最終生成的字節碼
圖片來源地址:http://notatube.blogspot.com/2010/12/project-lombok-creating-custom.html
為什麼Builder不能處理父類的成員變量
我們可以翻一下Lombok的源碼,Lombok對所有的註解都有兩套實現,javac和eclipse,由於我們的運行環境是Idea所以我們選擇javac的實現,javac版本的實現在lombok.javac.handlers.HandleBuilder#handle這個方法中
JavacNode parent = annotationNode.up();if (parent.get() instanceof JCClassDecl) { job.parentType = parent; JCClassDecl td = (JCClassDecl) parent.get(); ListBuffer<JavacNode> allFields = new ListBuffer<JavacNode>(); 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註解,站在抽象語法樹的角度,調用up方法得到的就是被註解修飾的類,也就是需要生成builder方法的類。
通過查看原始碼,@Builder註解是可以修飾類,構造函數和方法的,為了簡單起見,上面的代碼只截取了@Builder修飾類這一種情況,這段代碼關鍵的地方就在於調用HandleConstructor.findAllFields方法獲得類中所有的成員變量:
public static List<JavacNode> findAllFields(JavacNode typeNode, boolean evenFinalInitialized) { ListBuffer<JavacNode> fields = new ListBuffer<JavacNode>(); // 從抽象語法樹出發,遍歷類的所有的成員變量 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();}這段代碼比較簡單,就是對類中的成員變量做了過濾,比如說,靜態變量就不能被@Builder方法構造。有一個有意思的點,儘管$可以合法的出現在java的變量命名中,但是Lombok對這種變量做了過濾,因此變量名以$開始的也不能被@Builder構造,經過我們的驗證確實是這樣的。
如果我們用JDT AstView看一下ItemDTO的抽象語法樹結構,發現Java的抽象語法樹設計的確是每個類只包含顯式聲明的變量而不包括父類的成員變量(該插件支持點擊語法樹節點可以和源文件聯動,且數量只有4個和ItemDTO聲明的成員變量數量一致)
因為findAllFields方法是從當前類的抽象語法樹出發去找所有的成員變量,所以就只能找到當前類的成員變量,而訪問不到父類的成員變量
一個鏡像的問題就是,既然@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<A>". 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);}// 巴拉巴拉省略可以看到,這裡拿到了繼承的父類的抽象語法樹,並在後面的邏輯中進行了處理,這裡不再贅述