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

作者:明明如月学长, CSDN 博客专家,蚂蚁集团高级 Java 工程师,《性能优化方法论》作者、《解锁大厂思维:剖析《阿里巴巴Java开发手册》》、《再学经典:《EffectiveJava》独家解析》专栏作者。

热门文章推荐

一、背景

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

但是使用 lombok 也会造成很多问题,尤其 @Builder 有个很大的坑,已经见过好几次由于使用 @Builder 注解导致默认值失效的问题。
如果测试时没有在意这个问题,上线之后很容易出现故障。
大家使用时一定要注意这个问题。

二、复现问题

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

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class SomeConfig {
    
    
    private boolean isOpen = true;
    private String name;
    private int value = 20;
}

使用时仅设置 name ,那么打印出 config 对象将输出什么内容?

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 + ")";
        }
    }
}

大家可以看到,SomeConfigBuilderisOpenvalue 属性并没有使用我们想要设置的默认值。
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注解就能解决这个问题?
同样的,我们查看编译后的类长什么样子就一切都明白了。

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。


创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
在这里插入图片描述

欢迎加入我的知识星球,知识星球ID:15165241 一起交流学习。
https://t.zsxq.com/Z3bAiea 申请时标注来自CSDN。

欢迎加入我们的 slack 工作区,在里面可以对ai 和我进行提问。
https://join.slack.com/t/ai-yx51081/shared_invite/zt-1t8cp1lk3-ZMAFutZcN3PCW~8WQDGjPg

猜你喜欢

转载自blog.csdn.net/w605283073/article/details/130190814