【线上踩坑分享】 使用@Builder注解导致属性默认值丢失

@Builder使用遇到的坑

问题现象

问题现象大概如下面示例所示:

有一个Account对象,内部包含一个设置默认值为0type属性,但由于使用了Builder方式构建,最终导致默认值丢失。

java 复制代码
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {

    private Long id;

    private String name;

    private String type = "0";

}

// 测试方法
class Main {
    public static void main(String[] args) {
        Account account = Account.builder().name("Li").id(1L).build();
        System.out.println(account);
    }
}

测试结果输出:

txt 复制代码
Account(id=1, name=Li, type=null)

问题分析

通过反编译@Builder注解生成的内容可以看出,问题就在于build方法。

java 复制代码
public static class AccountBuilder {
    private Long id;
    private String name;
    private String type;

    AccountBuilder() {
    }

    public Account.AccountBuilder id(Long id) {
        this.id = id;
        return this;
    }

    public Account.AccountBuilder name(String name) {
        this.name = name;
        return this;
    }

    public Account.AccountBuilder type(String type) {
        this.type = type;
        return this;
    }
    
    public Account build() {
        return new Account(this.id, this.name, this.type);
    }

    public String toString() {
        return "Account.AccountBuilder(id=" + this.id + ", name=" + this.name + ", type=" + this.type + ")";
    }
}

解决方式

在有默认值的属性上加上@Builder.Default注解即可。

java 复制代码
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {

    private Long id;

    private String name;

    @Builder.Default
    private String type = "0";

}

再次反编译后发现(已过滤掉了部分无关代码内容),其实现方式就是通过$default$type方法来完成默认值的赋值操作。

java 复制代码
public class Account {
    private Long id;
    private String name;
    private String type;
    
    // 如果没有主动设置过type属性,则会通过调用此方法完成默认值设置
    private static String $default$type() {
        return "0";
    }

    public static Account.AccountBuilder builder() {
        return new Account.AccountBuilder();
    }

    public Account(Long id, String name, String type) {
        this.id = id;
        this.name = name;
        this.type = type;
    }

    public Account() {
        this.type = $default$type();
    }

    public static class AccountBuilder {
        private Long id;
        private String name;
        private boolean type$set;
        private String type$value;

        AccountBuilder() {
        }

        public Account.AccountBuilder id(Long id) {
            this.id = id;
            return this;
        }

        public Account.AccountBuilder name(String name) {
            this.name = name;
            return this;
        }

        public Account.AccountBuilder type(String type) {
            this.type$value = type;
            this.type$set = true;
            return this;
        }

        public Account build() {
            String type$value = this.type$value;
            if (!this.type$set) {
                type$value = Account.$default$type();
            }

            return new Account(this.id, this.name, type$value);
        }

        public String toString() {
            return "Account.AccountBuilder(id=" + this.id + ", name=" + this.name + ", type$value=" + this.type$value + ")";
        }
    }
}

延伸思考

实际上@Builder使用还会遇到其他问题

比如如果没有@NoArgsConstructor注解,就会缺少无参的构造方法,这严重违反了代码编写规范。

  • 如果只有@Data注解,会生成无参构造方法。
  • 如果只有@Builder注解,会生成全属性的构造方法,但没有无参构造方法。
  • 如果@Data@Builder一起使用,哪怕你手动添加了无参构造方法或者添加了@NoArgsConstructor注解,最终在程序运行时依然会报错(编译时无问题)。
  • 实际上通常情况下你必须4个注解一起加上使用@Builder、@Data、@AllArgsConstructor、@NoArgsConstructor

关于上面这些问题,你可以说是使用者本身对于@Builder注解的理解不深,但笔者认为在实际的业务开发中,难免会因为各种各样的原因而导致问题产生,对于一个方法的使用,不单单是要要求使用者能够完全了解,更重要的是方法本身在使用时会不会出现让使用者容易忽视的条件。

替代方案

我相信大多数使用@Builder主要是因为链式编程所带来的便捷性,实际上笔者更推荐使用@Accessors,它同样可以实现链式编程,同时还可以避免多创建一个Builder对象,更重要的是可以避免@Builder的坑。

只需要通过@Accessors(chain = true)@Data两个注解即可轻松实现。

java 复制代码
//@Builder
//@Data
//@AllArgsConstructor
//@NoArgsConstructor
@Accessors(chain = true)
@Data
public class Account {

    private Long id;

    private String name;

    // @Builder.Default
    private String type = "0";

}

class Main {
    public static void main(String[] args) {
//        Account account = Account.builder().name("Li").id(1L).build();
//        System.out.println(account);
        
        Account ac = new Account().setId(1L).setName("Li");
        System.out.println(ac);
        
    }
}
相关推荐
菜鸟233号5 小时前
力扣213 打家劫舍II java实现
java·数据结构·算法·leetcode
panzer_maus5 小时前
Redis简单介绍(3)-持久化的实现
java·redis·mybatis
毕设源码-邱学长5 小时前
【开题答辩全过程】以 民宿在线预定平台的设计与实现为例,包含答辩的问题和答案
java·eclipse
蓝眸少年CY5 小时前
(第十二篇)spring cloud之Stream消息驱动
后端·spring·spring cloud
码界奇点5 小时前
基于SpringBoot+Vue的前后端分离外卖点单系统设计与实现
vue.js·spring boot·后端·spring·毕业设计·源代码管理
不会Android的潘潘6 小时前
受限系统环境下的 WebView 能力演进:车载平台 Web 渲染异常的根因分析与优化实践
android·java·前端·aosp
建军啊6 小时前
java web常见lou洞
android·java·前端
阳无6 小时前
宝塔部署的前后端项目从IP访问改成自定义域名访问
java·前端·部署
点云SLAM6 小时前
C++(C++17/20)最佳工厂写法和SLAM应用综合示例
开发语言·c++·设计模式·c++实战·注册工厂模式·c++大工程系统
Pluchon6 小时前
硅基计划4.0 算法 动态规划进阶
java·数据结构·算法·动态规划