@Builder不好用,试试@SuperBuilder
共 5254字,需浏览 11分钟
·
2021-01-09 19:26
相信 Lombok 插件大家一定不会陌生,一个常用的注解是:@Builer
, 它可以帮我们快速实现一个builder
模式。以常见的商品模型为例:
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public 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
@NoArgsConstructor
public class BaseDTO {
/**
* 业务身份
*/
private String bizType;
/**
* 场景
*/
private String scene;
}
这时候我们再使用Builder
注解就会发现,在子类中无法通过builder
方法构造父类中的成员变量
给BaseDTO
上加上Builder
注解也不会有任何效果。事实上,Builder
注解只管承接注解的这个类,而不会管他的父类或者子类。如果真的是这样的话,遇到有继承的类,只好又打回原形,写一堆的setter
方法了。
试试SuperBuilder
吧
这个问题在lombok
v1.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 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
注解,站在抽象语法树的角度,调用up
方法得到的就是被注解修饰的类,也就是需要生成builder
方法的类。
通过查看源代码,@Builder
注解是可以修饰类,构造函数和方法的,为了简单起见,上面的代码只截取了@Builder
修饰类这一种情况,这段代码关键的地方就在于调用HandleConstructor.findAllFields
方法获得类中所有的成员变量:
public static List findAllFields(JavacNode typeNode, boolean evenFinalInitialized) {
ListBuffer fields = new ListBuffer();
// 从抽象语法树出发,遍历类的所有的成员变量
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".
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上的一些优质项目搜罗
推荐👍:啪地一下,很快啊!已经 一年了!
我是Guide哥,Java后端开发,拥抱开源,喜欢烹饪,自由的少年。一个喜欢使用Lombok的技术人。我们下期再见