Bug记录:Lombok @Builder 注解的两大陷阱及解决方案
问题概述:
Lombok 的 @Builder 注解极大地简化了“建造者模式”的代码编写,但其默认行为存在两个非常隐蔽的陷阱:
陷阱一:无参构造函数消失
现象:当一个类同时使用 @Data 和 @Builder 后,原本可用的无参构造函数 new Entity() 突然不可用,编译报错。
陷阱二:字段默认值(初始化)失效
现象:在字段声明时设置的默认值(如 String status = “open”;),通过 Builder 创建对象时,若未显式设置该字段,其值将为 null 而非预期的 “open”。
陷阱一:无参构造函数消失
复现步骤
编写一个同时使用 @Data 和 @Builder 的类。
@Data
@Builder
public class User {private String name;private Integer age;
}
尝试在其他地方使用无参构造
User user = new User(); // 编译错误:找不到符号 User()
原因分析
-
@Data 会生成一个 @RequiredArgsConstructor。如果类中没有 final 或@NonNull 的字段,这个构造器就是无参的。此时,单独使用 @Data 是完全可以无参构造的。
-
@Builder 注解需要基于一个全参构造函数来工作。当它发现你没有显式提供任何构造器时,它会主动生成一个私有的、包含所有字段的全参构造函数。
-
@Builder 的生成行为取代了 @Data 生成 @RequiredArgsConstructor 的逻辑。最终,编译后的类中只剩下 @Builder 生成的私有全参构造器,导致无参构造器确实“消失”了。
解决方案
显式声明所需的全部构造器,避免 Lombok 的默认行为产生冲突。
@Data
@Builder
@NoArgsConstructor // 显式指定:我需要无参构造
@AllArgsConstructor // 显式指定:我需要全参构造(让Builder直接使用这个现成的)
public class User {private String name;private Integer age;
}
陷阱二:字段默认值(初始化)失效
复现步骤
编写一个带有字段默认值的类并使用 Builder。
@Data
@Builder
public class Order {private Long id;private String status = "open"; // 期望的默认值
}
使用 Builder 创建对象,但不设置 status 字段。
Order order = Order.builder().id(1L).build();
System.out.println(order.getStatus()); // 输出:null (预期是 "open")
原因分析
-
字段的默认值 = "open"是在类的构造函数内部执行的。
-
@Builder 的 build() 方法本质上是调用了类的全参构造函数,并将 Builder 对象内部的字段值作为参数传入。关键点在于:构造函数会无条件使用传入的参数。
-
如果你没有调用 .status(…) 方法,Builder 对象内部的 status 字段值就是 null。在调用 build() 时,执行的是 new Order(id, status),这个 null 被传入了构造函数,完全覆盖了类定义中 = “open” 的初始化操作。
解决方案
使用 Lombok 提供的 @Builder.Default 注解。
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Order {private Long id;@Builder.Default // 使用此注解标记private String status = "open";
}
@Builder.Default 的原理:它会指示 Lombok 在生成的 Builder 类中,预先将这个字段初始化为指定的默认值。如果后续没有显式设置该字段,build() 方法就会使用这个预先存在的默认值,而不是 null。