千万不要再随便使用 lombok 的 @Builder 了!

2023-04-21 19:58:01 浏览数 (2)

一、背景

现在很多大厂的员工也很喜欢使用 lombok,有了 lombok 加持之后代码更加 “简洁”。

但是使用 lombok 也会造成很多问题,尤其 @Builder 有个很大的坑,已经见过好几次由于使用 @Builder 注解导致默认值失效的问题。

如果测试时没有在意这个问题,上线之后很容易出现故障。

大家使用时一定要注意这个问题。

二、复现问题

我们定义 SomeConfig 对象,对其中的 isOpenvalue 设置默认值。

代码语言:javascript复制
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 生成的,所以我们不需要手动编写。

代码语言:javascript复制
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   ")";
        }
    }
}

大家可以看到,SomeConfigBuilderisOpenvalue 属性并没有使用我们想要设置的默认值。

build 方法时, SomeConfigBuilder 中调用全参的构造方法来构造 SomeConfig 对象。

四、解决办法

4.1 解决方式

使用 @Builder.Default注解就可以解决这个问题。

代码语言:javascript复制
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

0 人点赞