Lombok 的 @Builder 和手写建造者有什么区别?
简化版
Lombok 的 @Builder 可以自动生成 Builder 样板代码,适合字段较多但构建规则不复杂的对象。手写 Builder 更适合需要复杂校验、默认值、防御性拷贝、构建步骤控制或更强可读性的场景。
详细版
@Builder 的优势是减少样板代码:
@Builder
public class UserQuery {
private Long userId;
private String status;
private Integer pageNo;
private Integer pageSize;
}
调用时:
UserQuery query = UserQuery.builder()
.userId(1001L)
.status("ACTIVE")
.pageNo(1)
.pageSize(20)
.build();
它的优点:
- 代码少;
- 调用可读性好;
- 适合 DTO、查询条件、简单配置对象;
- 和 Lombok 的
@Getter、@Value等注解搭配方便。
但它也有局限:
- 默认值需要配合
@Builder.Default,否则字段初始化值可能不按预期生效; - 复杂校验不如手写 Builder 清晰;
- 对集合字段要注意
@Singular和不可变处理; - 生成代码隐藏在编译期,新人排查问题可能不直观;
- 过度使用可能让领域对象的构建规则变得隐式。
面试回答时可以说:@Builder 是工程效率工具,不是模式理解的替代品。简单对象可以用 Lombok,复杂对象建议手写关键构建逻辑。
完整版教学
一、Lombok @Builder 到底做了什么
@Builder 本质上是在编译期帮你生成一个 Builder 类。它会生成类似下面的代码:
public static UserQueryBuilder builder() {
return new UserQueryBuilder();
}
public static class UserQueryBuilder {
private Long userId;
private String status;
public UserQueryBuilder userId(Long userId) {
this.userId = userId;
return this;
}
public UserQuery build() {
return new UserQuery(userId, status);
}
}
所以它不是一种新模式,而是帮我们少写建造者模式的样板代码。
理解这一点很重要。面试官问 Lombok Builder 时,通常不是想听“加个注解就行”,而是想看你是否知道它生成了什么、适合什么、不适合什么。
二、默认值是高频坑
很多人会写:
@Builder
public class ClientConfig {
private int timeoutMs = 3000;
}
然后以为:
ClientConfig config = ClientConfig.builder().build();
timeoutMs 会是 3000。实际使用 Lombok 时,这类默认值需要特别注意,通常要配合 @Builder.Default:
@Builder
public class ClientConfig {
@Builder.Default
private int timeoutMs = 3000;
}
原因是 Lombok 生成的 Builder 会持有自己的字段,构建时使用 Builder 字段生成对象;普通字段初始化不一定等价于 Builder 默认值。
这个点在面试中很容易加分,因为它说明你不是只会贴注解。
三、复杂校验为什么手写更好
如果只是查询条件,@Builder 很舒服:
OrderQuery.builder().status("PAID").pageSize(20).build();
但如果对象有复杂规则,手写 Builder 更清楚。例如:
- 开启 TLS 必须提供证书;
- 重试次数和超时时间必须匹配;
- 起止时间必须同时存在且开始时间小于结束时间;
- 集合字段需要不可变拷贝;
- 某些字段只能二选一。
这些规则虽然也可以通过自定义构造器或额外方法处理,但如果规则很多,隐藏在 Lombok 生成代码周围反而会降低可读性。
手写 Builder 可以把规则明确写在 build():
public ClientConfig build() {
if (tlsEnabled && certificate == null) {
throw new IllegalStateException("certificate required");
}
return new ClientConfig(this);
}
这段代码让构建规则一眼可见。
四、Lombok Builder 的使用边界
比较稳的工程经验是:
- 简单 DTO、查询条件、测试对象:可以用
@Builder; - 核心领域对象、复杂配置对象、涉及不变量的对象:优先手写;
- 对外 SDK 或公共组件:谨慎使用 Lombok,避免调用方理解成本和依赖约束;
- 集合字段和默认值:一定要单独检查。
如果团队大量使用 Lombok,要保证 IDE 插件、编译配置、代码规范都一致,否则新人会看到“源码里没有方法,但代码能调用”的魔法感。
五、展开生成代码检查默认值与集合语义
评审 @Builder 时应像看手写代码一样查看 delombok 结果:Builder 有独立字段和“是否显式设置”标记,普通字段初始化不等于 Builder 默认值;集合的 @Singular 又会生成逐项收集与构建期不可变处理。
源码: @Builder class ClientConfig
编译期生成 ClientConfigBuilder
builder.timeoutMs 字段有自己的初始语义
未加 @Builder.Default 时普通字段初值可能不进入 Builder 路径
加 @Builder.Default 后生成 set 标记与默认提供方法
@Singular("header") 生成 header(k,v)/headers(map)
build() 汇总集合
复杂跨字段规则仍需显式代码
delombok/IDE 可查看真实生成结果
测试 builder().build() 的默认对象
这条时间线把“可变构建阶段”和“稳定成品阶段”分开。Builder 可以反复接收参数,但 Product 只有在所有规则通过后才出现;若 Product 在第一步就被 new 出来再逐项修改,就仍然存在半初始化对象泄漏的窗口。
六、构建契约与对象不变量
| 评审维度 | 本题结论 |
|---|---|
| 必填信息 | Lombok 不会替业务自动定义必填字段,应通过自定义构造/校验表达。 |
| 默认值 | 需要 Builder 路径默认值时使用 @Builder.Default 并用测试确认版本行为。 |
| 跨字段规则 | 复杂不变量应放显式构造器、工厂或自定义 build 附近,不能假设注解生成。 |
| 可变引用处理 | 集合结合 @Singular 仍需确认元素可变性和所需深拷贝。 |
| Builder 生命周期 | 生成的 Builder 仍是普通可变对象,不自动线程安全。 |
| Product 交付保证 | 是否不可变取决于字段、构造与暴露 API,不取决于单独 @Builder。 |
build() 不是形式上的结束标记,而是对象从“参数集合”变成“合法业务值”的原子边界。单字段输入可以提前拒绝明显错误,跨字段规则必须等信息齐全后判断,Product 私有构造器还应保留必要兜底,避免未来新增创建入口绕过不变量。
七、与构造器、JavaBean 和工厂的选择边界
- 字段少且全部必填时,短构造器或 record 通常比 Builder 更直接。
- 字段多、可选项多、同类型参数易错时,命名步骤能显著提升调用可读性。
- JavaBean setter 适合某些框架绑定,但对象可能在设置完成前就被观察到。
- Builder 关注“同一种复杂对象怎样组装”;工厂模式关注“创建哪一种产品实现”。
- 两者可以组合:工厂选择具体产品族或 Builder,Builder 再完成复杂组装。
- 经典 Director 只有在固定步骤序列需要复用或存在多种表示时才有价值。
- 链式
return this只是语法,校验、默认值、拷贝和收口才是设计语义。 - 若 Builder 比 Product 规则还复杂,应重新拆分对象职责,而不是继续堆方法。
DTO、测试数据和简单查询对象适合 Lombok;核心领域对象、复杂校验和公共 SDK 优先显式手写规则。
八、交付前的代码与测试检查
- 缺少每个必填字段分别调用
build(),应得到明确且稳定的异常。 - 完全不设置可选字段,核对默认值来自唯一权威位置。
- 传入最小值、最大值和越界值,确认范围判断没有反向或 off-by-one。
- 构造两个互相冲突的字段组合,确认只在信息完整时执行跨字段校验。
- 传入集合或数组后修改原引用,已构建 Product 不应跟着变化。
- 若 getter 返回可变数据,再尝试修改返回值,内部状态仍应保持不变。
- 连续调用同一个 Builder 两次,确认语义是明确允许复制还是文档明确禁止复用。
- 两线程共享一个 Builder 做压力测试应被禁止或证明安全,不能靠偶然结果。
- 若使用 Lombok,检查生成代码、默认值、
@Singular、构造器可见性和框架兼容。 - 若迁移旧 API,比较默认值、异常类型、序列化字段和所有旧调用点行为。
记忆钩子:@Builder 省的是样板,不会替你发明默认值、不变量和所有权。
九、常见误区与追问
- 误区:字段声明处的初值一定会成为 Builder 默认值。 Lombok Builder 路径通常需
@Builder.Default才表达该语义。 - 误区:加 @Builder 后对象自动不可变。 仍取决于字段 final、setter 和可变引用拷贝。
- 误区:@Singular 等于深拷贝集合元素。 它主要处理容器构建,元素自身可变性仍存在。
- 追问:如何知道 Lombok 生成了什么? 使用 delombok、IDE 结构视图或查看编译产物。
- 追问:继承对象用什么?
@SuperBuilder可支持层次,但父子配置和版本兼容更复杂。 - 追问:为什么公共库要谨慎? 生成 API、注解处理依赖和二进制兼容会成为对外契约的一部分。
十、加强记忆
@Builder 是省代码的工具,手写 Builder 是表达规则的设计。简单对象用 Lombok 提效,复杂对象把规则写清楚;尤其记住默认值、集合字段和复杂校验这三个坑。