一、背景
现在很多大厂的员工也很喜欢使用 lombok,有了 lombok 加持之后代码更加 “简洁”。
但是使用 lombok 也会造成很多问题,尤其 @Builder
有个很大的坑,已经见过好几次由于使用 @Builder
注解导致默认值失效的问题。
如果测试时没有在意这个问题,上线之后很容易出现故障。
大家使用时一定要注意这个问题。
二、复现问题
我们定义 SomeConfig
对象,对其中的 isOpen
和 value
设置默认值。
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class SomeConfig {
private boolean isOpen = true;
private String name;
private int value = 20;
}
使用时仅设置 name ,那么打印出 config 对象将输出什么内容?
代码语言:javascript复制public class LombokDemo {
public static void main(String[] args) {
SomeConfig config = SomeConfig.builder().name("test").build();
System.out.println(config);
}
}
输出结果:
SomeConfig(isOpen=false, name=test, value=0)
为什么我们设置的默认值失效了??
三、原因揭秘
想了解为什么会这样,我们只需要查看使用 lombok 的注解后 SomeConfig 的 class 文件长啥样就明白了。
@Builder
通过 lombok 的注解处理器,在编译时自动生成了一个静态内部类,这个内部类就是所谓的 builder 类,它包含了和被注解的类中的属性一一对应的 setter 方法,并且在 build() 方法中返回一个被注解的类的对象。这个 builder 类的代码实现是通过 lombok 生成的,所以我们不需要手动编写。
public class SomeConfig {
private boolean isOpen = true;
private String name;
private int value = 20;
SomeConfig(boolean isOpen, String name, int value) {
this.isOpen = isOpen;
this.name = name;
this.value = value;
}
public static SomeConfigBuilder builder() {
return new SomeConfigBuilder();
}
public boolean isOpen() {
return this.isOpen;
}
public String getName() {
return this.name;
}
public int getValue() {
return this.value;
}
public void setOpen(boolean isOpen) {
this.isOpen = isOpen;
}
public void setName(String name) {
this.name = name;
}
public void setValue(int value) {
this.value = value;
}
public boolean equals(Object o) {
// 省略
}
protected boolean canEqual(Object other) {
return other instanceof SomeConfig;
}
public int hashCode() {
// 省略
}
public String toString() {
return "SomeConfig(isOpen=" this.isOpen() ", name=" this.getName() ", value=" this.getValue() ")";
}
public static class SomeConfigBuilder {
private boolean isOpen;
private String name;
private int value;
SomeConfigBuilder() {
}
public SomeConfigBuilder isOpen(boolean isOpen) {
this.isOpen = isOpen;
return this;
}
public SomeConfigBuilder name(String name) {
this.name = name;
return this;
}
public SomeConfigBuilder value(int value) {
this.value = value;
return this;
}
public SomeConfig build() {
return new SomeConfig(this.isOpen, this.name, this.value);
}
public String toString() {
return "SomeConfig.SomeConfigBuilder(isOpen=" this.isOpen ", name=" this.name ", value=" this.value ")";
}
}
}
大家可以看到,SomeConfigBuilder
中 isOpen
和 value
属性并没有使用我们想要设置的默认值。
用 build
方法时, SomeConfigBuilder
中调用全参的构造方法来构造 SomeConfig
对象。
四、解决办法
4.1 解决方式
使用 @Builder.Default
注解就可以解决这个问题。
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class SomeConfig {
@Builder.Default
private boolean isOpen = true;
private String name;
@Builder.Default
private int value = 20;
}
修改后的输出结果:
SomeConfig(isOpen=true, name=test, value=20)
4.2 底层原理
为什么对想设置默认值的属性加上 @Builder.Default
注解就能解决这个问题?
同样的,我们查看编译后的类长什么样子就一切都明白了。
代码语言:javascript复制public class SomeConfig {
private boolean isOpen;
private String name;
private int value;
private static boolean $default$isOpen() {
return true;
}
private static int $default$value() {
return 20;
}
SomeConfig(boolean isOpen, String name, int value) {
this.isOpen = isOpen;
this.name = name;
this.value = value;
}
public static SomeConfigBuilder builder() {
return new SomeConfigBuilder();
}
public boolean isOpen() {
return this.isOpen;
}
public String getName() {
return this.name;
}
public int getValue() {
return this.value;
}
public void setOpen(boolean isOpen) {
this.isOpen = isOpen;
}
public void setName(String name) {
this.name = name;
}
public void setValue(int value) {
this.value = value;
}
public boolean equals(Object o) {
// 省略
}
protected boolean canEqual(Object other) {
return other instanceof SomeConfig;
}
public int hashCode() {
// 省略
}
public String toString() {
boolean var10000 = this.isOpen();
return "SomeConfig(isOpen=" var10000 ", name=" this.getName() ", value=" this.getValue() ")";
}
public static class SomeConfigBuilder {
private boolean isOpen$set;
private boolean isOpen$value;
private String name;
private boolean value$set;
private int value$value;
SomeConfigBuilder() {
}
public SomeConfigBuilder isOpen(boolean isOpen) {
this.isOpen$value = isOpen;
this.isOpen$set = true;
return this;
}
public SomeConfigBuilder name(String name) {
this.name = name;
return this;
}
public SomeConfigBuilder value(int value) {
this.value$value = value;
this.value$set = true;
return this;
}
public SomeConfig build() {
boolean isOpen$value = this.isOpen$value;
if (!this.isOpen$set) {
isOpen$value = SomeConfig.$default$isOpen();
}
int value$value = this.value$value;
if (!this.value$set) {
value$value = SomeConfig.$default$value();
}
return new SomeConfig(isOpen$value, this.name, value$value);
}
public String toString() {
return "SomeConfig.SomeConfigBuilder(isOpen$value=" this.isOpen$value ", name=" this.name ", value$value=" this.value$value ")";
}
}
}
每个设置默认值的属性都会在 Builder
中加上是否设置的标记,如果没有主动设置值,则调用 SomeConfig
中的默认值的静态方法进行赋值,然后再调用 SomeConfig
全参构造方法构造该对象。
五、Builder 注解的副作用
正如之前在 《同学你根本不懂 Builder 设计模式!》 一文中我们也讲到的, Builder 注解存在一些副作用:
(1)如果你在类上使用了 @Builder
注解,那么你需要手动添加一个无参构造函数,否则有些序列化框架需要通过 newInstance
构造对象时会报错。
(2)如果你在类上使用了 @Builder
注解,那么你不能再在构造函数或方法上使用 @Builder
注解,否则会导致重复生成构造器类。
(3)如果你想给某个属性设置一个默认值,那么你需要在属性上使用 @Builder.Default
注解,否则默认值会被忽略。
(4)如果你想让子类继承父类的属性,那么你需要在子类的全参构造函数上使用 @Builder
注解,并且在父类上使用 @AllArgsConstructor
注解,否则子类的构造器类不会包含父类的属性。
六、总结
虽然很多人吐槽,“面试造轮子,入职拧螺丝”,实际上一定的理论基础是有必要的。
很多知识点只有懂原理才能少趟坑。建议大家使用 lombok 的注解时,工作之余偶尔看下编译后的类长什么样子,这样有助于避坑。
lombok 的 @Builder
注解虽然好用,但不要“贪杯”。使用 @Builder
一定要注意它的副作用,避免出现潜在的 BUG