背景
惯例说一下背景,对于那些复杂的接口或者大的查询接口,一般我们会在入参定义一个option,然后将查询域分割成几部分,由调用方指定需要查询的域。今天在调用其他团队某个接口的时候发现对方定义的Option里面分成了十几个选项,但是没有提供链式调用,这样每次set值就要好几行代码,使用起来很不方便,代码也不美观。所以在己方代码里做了一层封装,定义了一个相同的Option类,标注了Lombok的@Builder注解,在真正调用对方接口的时候利用Bean的copy工具直接copy。
问题
咋看觉得应该没有什么问题,但是实际使用发现有个问题,对方的Option类都提供了默认值false,我们的也提供了默认值false。使用的代码大致如下:
@Data
@Builder
public class MyOption implements Serializable {
private static final long serialVersionUID = -1L;
private Boolean needA = Boolean.FALSE;
private Boolean needB = Boolean.FALSE;
}
@Data
public class OtherOption implements Serializable {
private static final long serialVersionUID = -1L;
private Boolean needA = Boolean.FALSE;
private Boolean needB = Boolean.FALSE;
}
MyOption option = MyOption.builder().needA(true).build();
OtherOption otherOption = BeanUtils.convert(option, OtherOption.class);
... // 使用otherOption调用接口
结果发现最终得到的otherOption这个对象中needB是null,而不是预想中的false。
解决
一开始以为是convert方法有问题,因为在这个方法内部,我们是通过Class的newInstance方法创建目标bean的实例的。搞了个测试案例跑了一下,发现没有问题。所以就怀疑Lombok的@Builder注解了,记得这个注解是会定义一个静态内部类的,最终调用build()方法的时候会将内部类的属性都设置到外部类中。试了一下果然是这个问题。
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
MyOption option = MyOption.builder().needA(true).build();
System.out.println("@Builder注解:" + option);
System.out.println("newInstance()方法:" + MyOption.class.newInstance());
OtherOption convert = BeanUtils.convert(option, OtherOption.class);
System.out.println("BeanUtils.convert方法:" + convert);
}
执行结果:
@Builder注解:MyOption(needA=true, needB=null)
newInstance()方法:MyOption(needA=false, needB=false)
BeanUtils.convert方法:OtherOption(needA=true, needB=null)
那怎么解决呢?其实Lombok考虑到了这个问题,@Builder.Default注解就是用来解决这个问题的,看一下官方的注释:
The field annotated with {@code @Default} must have an initializing expression; that expression is taken as the default to be used if not explicitly set during building.
大致意思就是注解的属性需要有初始值,这样在builder的时候就会将这个默认值设置到最后build()方法构造出来的对象中去。给MyOption的属性标上这个注解后再运行。
@Builder注解:MyOption(needA=true, needB=false)
newInstance()方法:MyOption(needA=null, needB=null)
BeanUtils.convert方法:OtherOption(needA=true, needB=false)
可以看到build出来的对象是有默认值了,但是通过newInstance()得到的实例却没有了默认值。
本来以为搞定了,只好查看编译后的class文件一谈究竟了,下面是标注了@Builder.Default注解后的MyOption类编译后的源码,这里省略了toString,hashCode,equals方法:
public class MyOption {
private Boolean needA;
private Boolean needB;
private static Boolean $default$needA() {
return Boolean.FALSE;
}
private static Boolean $default$needB() {
return Boolean.FALSE;
}
public static MyOption.MyOptionBuilder builder() {
return new MyOption.MyOptionBuilder();
}
public Boolean getNeedA() {
return this.needA;
}
public Boolean getNeedB() {
return this.needB;
}
public void setNeedA(Boolean needA) {
this.needA = needA;
}
public void setNeedB(Boolean needB) {
this.needB = needB;
}
public MyOption() {
}
public MyOption(Boolean needA, Boolean needB) {
this.needA = needA;
this.needB = needB;
}
public static class MyOptionBuilder {
private boolean needA$set;
private Boolean needA;
private boolean needB$set;
private Boolean needB;
MyOptionBuilder() {
}
public MyOption.MyOptionBuilder needA(Boolean needA) {
this.needA = needA;
this.needA$set = true;
return this;
}
public MyOption.MyOptionBuilder needB(Boolean needB) {
this.needB = needB;
this.needB$set = true;
return this;
}
public MyOption build() {
Boolean needA = this.needA;
if (!this.needA$set) {
needA = MyOption.$default$needA();
}
Boolean needB = this.needB;
if (!this.needB$set) {
needB = MyOption.$default$needB();
}
return new MyOption(needA, needB);
}
}
}
可以看到,MyOption类的默认值是通过方法来表达的,同时静态内部类MyOptionBuilder会有额外的字段来表示是否要应用外部类的默认值。而MyOption的无参构造方法啥都没干,所以说外部类如果直接通过newInstance()方法构造是不会有默认值的。
看一下没加注解@Builder.Default的MyOption的编译文件:
public class MyOption {
private Boolean needA;
private Boolean needB;
public static MyOption.MyOptionBuilder builder() {
return new MyOption.MyOptionBuilder();
}
public Boolean getNeedA() {
return this.needA;
}
public Boolean getNeedB() {
return this.needB;
}
public void setNeedA(Boolean needA) {
this.needA = needA;
}
public void setNeedB(Boolean needB) {
this.needB = needB;
}
public MyOption() {
this.needA = Boolean.FALSE;
this.needB = Boolean.FALSE;
}
public MyOption(Boolean needA, Boolean needB) {
this.needA = Boolean.FALSE;
this.needB = Boolean.FALSE;
this.needA = needA;
this.needB = needB;
}
public static class MyOptionBuilder {
private Boolean needA;
private Boolean needB;
MyOptionBuilder() {
}
public MyOption.MyOptionBuilder needA(Boolean needA) {
this.needA = needA;
return this;
}
public MyOption.MyOptionBuilder needB(Boolean needB) {
this.needB = needB;
return this;
}
public MyOption build() {
return new MyOption(this.needA, this.needB);
}
public String toString() {
return "MyOption.MyOptionBuilder(needA=" + this.needA + ", needB=" + this.needB + ")";
}
}
}
可以看到跟上面的区别就是,这里MyOption的无参构造方法是会设置默认值的。
总结
@Builder.Default解决了build()方法构造的属性默认值问题,但是也覆盖了无参构造方法原本的实现,谨慎使用吧。