你还在手写上百行 Builder 代码?Lombok 的
@Builder注解一行搞定。本文从建造者模式的本质出发,带你系统掌握@Builder的基础用法、进阶配置、继承处理和框架集成------不只是"怎么用",更说清楚"为什么这么用"和"什么时候不该用"。
适用版本 :Lombok 1.18.x+,Java 8+。@Builder 是 Lombok 中极为成熟的稳定特性,本文内容具有长期参考价值,不依赖某个特定小版本。
文章目录
-
- [1. 为什么需要 Builder?](#1. 为什么需要 Builder?)
-
- [1.1 构造器参数爆炸](#1.1 构造器参数爆炸)
- [1.2 Setter 泛滥也不是好方案](#1.2 Setter 泛滥也不是好方案)
- [1.3 建造者模式------优雅的中间路线](#1.3 建造者模式——优雅的中间路线)
- [2. 快速上手 @Builder](#2. 快速上手 @Builder)
-
- [2.1 基础用法](#2.1 基础用法)
- [2.2 编译后到底生成了什么?](#2.2 编译后到底生成了什么?)
- [2.3 放在构造器上------精确控制参与字段](#2.3 放在构造器上——精确控制参与字段)
- [3. 进阶配置------解决实际问题](#3. 进阶配置——解决实际问题)
-
- [3.1 @Builder.Default------默认值陷阱](#3.1 @Builder.Default——默认值陷阱)
- [3.2 @Singular------集合字段的优雅处理](#3.2 @Singular——集合字段的优雅处理)
- [3.3 toBuilder------从已有对象快速拷贝修改](#3.3 toBuilder——从已有对象快速拷贝修改)
- [3.4 自定义 Builder 元数据](#3.4 自定义 Builder 元数据)
- [4. 继承场景------@Builder 的局限性](#4. 继承场景——@Builder 的局限性)
-
- [4.1 问题演示](#4.1 问题演示)
- [4.2 解决方案:@SuperBuilder](#4.2 解决方案:@SuperBuilder)
- [4.3 为什么 @Builder 不直接支持继承?](#4.3 为什么 @Builder 不直接支持继承?)
- [5. 框架集成------别让反序列化变成坑](#5. 框架集成——别让反序列化变成坑)
-
- [5.1 Jackson 反序列化的核心问题](#5.1 Jackson 反序列化的核心问题)
- [5.2 标准解决方案](#5.2 标准解决方案)
- [5.3 其他框架兼容速查](#5.3 其他框架兼容速查)
- [6. 最佳实践与踩坑记录](#6. 最佳实践与踩坑记录)
-
- [6.1 DTO/VO 的推荐写法](#6.1 DTO/VO 的推荐写法)
- [6.2 常见编译错误速查](#6.2 常见编译错误速查)
- [6.3 JPA Entity 能用 @Builder 吗?](#6.3 JPA Entity 能用 @Builder 吗?)
- [6.4 什么时候不该用 @Builder](#6.4 什么时候不该用 @Builder)
- [7. 总结](#7. 总结)
-
- [7.1 核心要点回顾](#7.1 核心要点回顾)
- [7.2 适用边界](#7.2 适用边界)
- 参考

1. 为什么需要 Builder?
1.1 构造器参数爆炸
假设你在做一个用户系统,User 类起初只有 3 个字段:
java
public class User {
private String name;
private String email;
private String password;
public User(String name, String email, String password) {
this.name = name;
this.email = email;
this.password = password;
}
}
一切很美好。但随着需求叠加,字段膨胀到 8 个、10 个:
java
// ❌ 你能一眼看出每个参数的含义吗?
User user = new User("张三", "zhangsan@example.com", "123456",
"13800138000", "北京", "程序员", 10000.0, "ACTIVE");
问题来了:
- 可读性归零:第 5 个参数是 address 还是 job?不翻定义根本不知道
- 易错性飙升:相同类型的参数(如两个 String)调换位置,编译器不报错,运行时爆炸
- 可选参数难处理:大部分字段有默认值,但你必须传全所有参数,或者写 N 个重载构造器
1.2 Setter 泛滥也不是好方案
java
// ❌ 代码冗长,且对象在 setter 调用间隙处于"半成品"状态
User user = new User();
user.setName("张三");
user.setEmail("zhangsan@example.com");
user.setPassword("123456");
user.setPhone("13800138000");
// ... 更多 setter
Setter 方式有三个硬伤:
- 无法创建不可变对象 :字段必须去掉
final,对象可以被任意修改 - 对象状态不完整:构造和赋值分离,中间状态可能被其他线程读取
- 缺乏语义:看不出哪些字段是必须的、哪些是可选的
1.3 建造者模式------优雅的中间路线
建造者模式(Builder Pattern)的核心思想是:用 Builder 对象收集参数,最后一次性构建目标对象。
【设计模式】建造者模式(Builder Pattern)详解_建造者模式 (builder)-CSDN博客
java
// ✅ 链式调用,语义清晰
User user = new User.Builder()
.name("张三")
.email("zhangsan@example.com")
.age(25)
.salary(10000.0)
.build();
每个方法名就是字段名,一目了然。但问题也来了------手写 Builder 太痛苦了 。一个 10 字段的类,Builder 内部类要写上百行,每次加字段都要同步修改 Builder,纯体力活。这正是 @Builder 要解决的问题。
2. 快速上手 @Builder
2.1 基础用法
在类上添加 @Builder,Lombok 会在编译期自动生成建造者相关的所有代码:
java
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class User {
private String name;
private String email;
private int age;
private String phone;
private String address;
private String job;
private double salary;
}
使用方式和你期望的完全一样:
java
User user = User.builder()
.name("张三")
.email("zhangsan@example.com")
.age(25)
.salary(10000.0)
.build();
builder() 是 Lombok 自动生成的静态工厂方法,返回一个 UserBuilder 实例;每个字段方法返回 this,实现链式调用;build() 负责创建 User 对象。
2.2 编译后到底生成了什么?
理解 Lombok 生成的代码,能帮你更好地排错。以精简版的 User(只保留 name 和 age)为例,@Builder 编译后等价于:
java
public class User {
private String name;
private int age;
// Lombok 生成的全参构造器(默认包私有)
User(final String name, final int age) {
this.name = name;
this.age = age;
}
// 静态工厂方法------入口
public static UserBuilder builder() {
return new UserBuilder();
}
// 静态内部类------Builder
public static class UserBuilder {
private String name;
private int age;
UserBuilder() {}
// 每个字段对应一个 setter,返回 this 实现链式调用
public UserBuilder name(final String name) {
this.name = name;
return this;
}
public UserBuilder age(final int age) {
this.age = age;
return this;
}
// 终点:创建目标对象
public User build() {
return new User(this.name, this.age);
}
@Override
public String toString() {
return "User.UserBuilder(name=" + this.name + ", age=" + this.age + ")";
}
}
}
几个值得注意的细节:
- 构造器是包私有的(package-private),这是 Lombok 的默认行为,防止外部直接调用
- Builder 的每个 setter 参数都标了
final,避免在赋值前误修改 toString()也被自动覆写了,调试时打印 Builder 对象就能看到当前状态
2.3 放在构造器上------精确控制参与字段
如果 @Builder 放在类上会包含所有字段,但你只想暴露其中一部分,可以把 @Builder 放在构造器上:
java
public class User {
private String name;
private String email;
private String internalId; // 内部字段,不暴露给 Builder
private int age;
@Builder
public User(String name, String email, int age) {
// 只有这三个参数会出现在 Builder 方法中
this.name = name;
this.email = email;
this.age = age;
}
}
java
// Builder 只有 name / email / age 三个方法,internalId 不可通过 Builder 设置
User user = User.builder()
.name("张三")
.email("zhangsan@example.com")
.age(25)
.build();
💡 什么时候用:当类中有些字段是系统内部生成、计算得出,或需要特殊逻辑赋值时,放在构造器上可以精确控制 Builder 的"入口"。
3. 进阶配置------解决实际问题
基本用法只覆盖了 60% 的场景,剩下 40% 需要这些进阶能力。
3.1 @Builder.Default------默认值陷阱
初学者最容易踩的坑:你在字段上写了 private int age = 18;,以为 Builder 不传 age 时会默认 18,结果得到的却是 0。
原因是 :@Builder 生成的全参构造器会用 Builder 中的字段值直接覆盖类字段的初始值。如果 Builder 中 age 没被设置,它就是 Java 的零值(int 为 0,引用类型为 null),而非你写在字段上的 18。
@Builder.Default 就是用来解决这个问题的:
java
@Builder
public class User {
private String name; // 必须字段,不设默认值
@Builder.Default
private int age = 18; // 不传时默认 18
@Builder.Default
private String status = "ACTIVE"; // 不传时默认 "ACTIVE"
@Builder.Default
private List<String> roles = new ArrayList<>(); // 不传时默认空列表
}
java
User user = User.builder()
.name("张三")
// age、status、roles 都没传,但会使用 @Builder.Default 指定的默认值
.build();
System.out.println(user.getAge()); // 18
System.out.println(user.getStatus()); // "ACTIVE"
System.out.println(user.getRoles()); // [](空列表,不是 null)
⚠️ 坑 :如果不用
@Builder.Default,Lombok 在生成build()时不会读取字段上的初始值。@Builder.Default改变了代码生成逻辑------Builder 内部会用自己的默认值初始化字段,而不是依赖 Java 的零值。
3.2 @Singular------集合字段的优雅处理
当你有一个 List<String> tags 字段,你希望这样添加元素:
java
Article article = Article.builder()
.tag("Java")
.tag("Lombok")
.tag("Builder")
.build();
而不是每次都要构造一个完整的 List 再传进去。@Singular 就是做这个的:
java
@Builder
public class Article {
private String title;
@Singular
private List<String> tags;
@Singular
private Set<String> authors;
}
Lombok 会为每个 @Singular 字段生成四个方法:
| 生成的方法 | 作用 | 示例 |
|---|---|---|
tag(String tag) |
添加单个元素 | .tag("Java") |
tags(Collection<? extends String> tags) |
一次性覆盖整个集合 | .tags(Arrays.asList("a","b")) |
clearTags() |
清空已添加的元素 | .clearTags() |
内部 $tags 集合管理 |
编译后的实际集合字段 | 由 Lombok 处理,无需关心 |
方法名的单复数转换由 Lombok 自动处理,规则如下:
| 字段名 | 单数方法 | 复数方法 |
|---|---|---|
tags |
tag() |
tags() |
users |
user() |
users() |
children |
child() |
children() |
dataList |
dataList() |
dataList()(无法识别单数,保持不变) |
支持的集合类型覆盖了日常需求:List、Set、Map、SortedSet、SortedMap、NavigableSet、NavigableMap,以及 Guava 的 ImmutableList 等不可变集合。
java
// Map 类型的 @Singular
@Singular
private Map<String, Integer> scores;
// 生成 key/value/value/key 方法:
// .score("math", 95) ------ put 单个键值对
// .scores(someMap) ------ 覆盖整个 Map
// .clearScores() ------ 清空
3.3 toBuilder------从已有对象快速拷贝修改
很多场景下,你需要基于现有对象创建一个"大部分相同、只改一两个字段"的新对象。把这称为"增量修改"。toBuilder 就是为此设计的:
java
@Data
@Builder(toBuilder = true)
public class User {
private String name;
private String email;
private int age;
private String status;
}
java
// 原始对象
User original = User.builder()
.name("张三")
.email("old@example.com")
.age(25)
.status("ACTIVE")
.build();
// 基于原始对象,只修改邮箱------其他字段自动沿用
User updated = original.toBuilder()
.email("new@example.com")
.build();
// updated: name="张三", email="new@example.com", age=25, status="ACTIVE"
这在 DTO 转换、PUT 请求更新、审计记录复制等场景下非常实用------不用逐字段手动拷贝。
3.4 自定义 Builder 元数据
如果默认命名与项目规范冲突,Lombok 提供了一套完整的自定义参数:
java
@Builder(
builderClassName = "InnerBuilder", // Builder 类名
builderMethodName = "hiddenBuilder", // 静态工厂方法名
buildMethodName = "create", // 构建终点方法名
toBuilder = true // 开启 toBuilder
)
public class Order {
private String title;
private BigDecimal amount;
}
java
// 调用时使用自定义名称
Order order = Order.hiddenBuilder()
.title("测试订单")
.amount(new BigDecimal("99.99"))
.create(); // 终点是 create() 而不是 build()
四个参数的完整说明:
| 参数 | 说明 | 默认值 |
|---|---|---|
builderMethodName |
创建 Builder 的静态方法名 | builder |
buildMethodName |
从 Builder 构建目标对象的方法名 | build |
builderClassName |
Builder 内部类名 | XxxBuilder |
toBuilder |
是否生成 toBuilder() 方法 |
false |
一般场景不需要改这些,但在以下情况可能用到:
- 项目有统一的 Builder 命名规范
- 避免与已有方法名冲突
- 某些框架对方法名有特定要求
4. 继承场景------@Builder 的局限性
4.1 问题演示
@Builder 不处理继承------每个类的 Builder 只包含本类的字段,父类字段不会自动传递:
java
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
class Parent {
private String parentName;
}
@Getter
@Builder
class Child extends Parent {
private String childName;
}
java
// ❌ 编译错误!ChildBuilder 里没有 parentName 方法
Child child = Child.builder()
.childName("小明")
.parentName("大明") // 这行编译不通过
.build();
4.2 解决方案:@SuperBuilder
Lombok 1.18 引入了 @SuperBuilder,专门解决继承体系下的 Builder 生成:
java
import lombok.Getter;
import lombok.experimental.SuperBuilder;
@Getter
@SuperBuilder
class Parent {
private String parentName;
}
@Getter
@SuperBuilder
class Child extends Parent {
private String childName;
}
java
// ✅ 现在可以同时设置父类和子类字段
Child child = Child.builder()
.parentName("大明")
.childName("小明")
.build();
⚠️ 重要约束 :
@SuperBuilder必须同时加在父类和所有子类 上,漏掉任何一环都会导致编译错误。另外,@SuperBuilder生成的代码比@Builder更复杂------它内部使用了泛型自引用模式(ParentBuilder<C extends ..., B extends ...>),这也是为什么它现在还放在lombok.experimental包下,虽然实际已经相当稳定。
4.3 为什么 @Builder 不直接支持继承?
这是一个合理的设计选择。@Builder 的默认行为是生成一个全参构造器传给 Builder。如果它支持继承,就需要处理:
- 父类字段如何传递给子类 Builder
- 多级继承下 Builder 之间的类型关系
- 不同类中 Builder 方法命名的冲突
这些问题在 @SuperBuilder 中通过泛型契约得到了解决,代价是生成代码更复杂。对于大多数不涉及继承的场景,@Builder 保持了简洁性。
5. 框架集成------别让反序列化变成坑
5.1 Jackson 反序列化的核心问题
一旦在 DTO 上加 @Builder,配合 Spring Boot 接收 JSON 请求体时,最常见的报错是:
plain
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `com.example.User`
(no Creators, like default constructor, exist):
cannot deserialize from Object value
根因分析 :Jackson 反序列化时,默认通过无参构造器创建对象,再通过 setter 或反射给字段赋值。但 @Builder 只生成全参构造器 (没有无参构造器),且通常配合 @Data 或其他只读设计使用。Jackson 找不到无参构造器,反序列化失败。
5.2 标准解决方案
组合四个注解,一次性解决所有问题:
java
@Data // getter/setter/toString/equals/hashCode
@Builder // Builder 模式
@NoArgsConstructor // 给 Jackson 的无参构造器
@AllArgsConstructor // 给 @Builder 的全参构造器
public class UserDTO {
private String name;
private String email;
@Builder.Default
private String status = "ACTIVE";
@Singular
private List<String> tags;
}
四个注解各司其职,互不冲突:
| 注解 | 谁在用 | 如果没有 |
|---|---|---|
@Builder |
业务代码手动构建对象 | 没了 Builder 的便利性 |
@NoArgsConstructor |
Jackson / Hibernate 反序列化 | 反序列化报错 |
@AllArgsConstructor |
@Builder 的 build() 方法 |
编译错误(Builder 需要全参构造器) |
@Data |
通用 getter/setter | 手动写或用 @Getter @Setter |
5.3 其他框架兼容速查
| 框架 | 兼容情况 | 注意事项 |
|---|---|---|
| Jackson | 需要 @NoArgsConstructor + @AllArgsConstructor |
见上文 |
| MyBatis | 可通过全参构造器映射 | ResultMap 中 useActualParamName 需开启 |
| MapStruct | 完全兼容 | MapStruct 会通过 Builder 的 setter 或构造器映射 |
| Hibernate | 谨慎使用 | JPA Entity 涉及代理类,见 6.3 节 |
| Spring Data JPA | 查询结果映射可用 | @Query 结果映射到 DTO 时,需全参构造器 |
6. 最佳实践与踩坑记录
6.1 DTO/VO 的推荐写法
经过前面的分析,这套注解组合是最稳妥的:
java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
private String name;
private String email;
@Builder.Default
private String status = "ACTIVE";
@Singular
private List<String> roles;
}
一句话记法:数据类四条注解,序列化默认值各司其职。
6.2 常见编译错误速查
| 错误现象 | 原因 | 解决 |
|---|---|---|
build() 找不到方法 |
Builder 类里缺该方法 | 检查是否错误覆盖了 buildMethodName |
| Builder 不包含某字段 | @Builder 放在构造器上,只含构造器参数 |
改为放在类上,或把字段加入构造器 |
| 默认值不生效 | 没加 @Builder.Default |
给需要默认值的字段加上 |
| 继承中父类字段丢失 | 用了 @Builder 而不是 @SuperBuilder |
父子类全部改为 @SuperBuilder |
@Builder.Default 不生效 |
同时用了 @Data 覆盖了字段初始化 |
去掉字段上的 final 或在构造器中初始化 |
6.3 JPA Entity 能用 @Builder 吗?
能用,但建议谨慎。 JPA Entity(Jakarta Persistence API)有特殊约束:
- Hibernate 要求 Entity 有无参构造器(至少
protected) - Hibernate 代理类基于继承,可能与 Builder 的某些行为冲突
@Builder.Default会和 JPA 的字段默认值产生微妙的交互
如果一定要在 Entity 上用:
java
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA 要求,但限制访问
@AllArgsConstructor(access = AccessLevel.PRIVATE) // Builder 需要,但禁止外部调用
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Builder.Default
@Enumerated(EnumType.STRING)
private Status status = Status.ACTIVE;
@Column(nullable = false, length = 50)
private String name;
@Column(length = 100)
private String email;
}
⚠️ 风险提示 :在 JPA Entity 上使用
@Builder可能导致:
@Builder.Default初始化的集合被 Hibernate 的 PersistentCollection 替换后行为异常- 通过 Builder 创建的 Entity 如果没有 id,
save()时可能插入而非更新(取决于 JPA 实现)- 建议:Entity 层保持简单,用 DTO + Builder + 转换层来处理复杂构造逻辑,让 Entity 只负责持久化。
6.4 什么时候不该用 @Builder
| 场景 | 原因 |
|---|---|
| 只有 1-2 个字段的类 | Builder 的复杂度超过了便利性,直接用构造器更简单 |
| 字段全部是必填且无默认值 | 构造器 + 参数校验更直接,Builder 弱化了"哪些是必填"的语义 |
| 需要编译期强制约束 | Builder 无法强制调用某些方法(如 name() 必须调),这是运行时校验的责任 |
| 极高性能敏感场景 | Builder 引入了一次额外对象分配,不过在 JIT 优化下通常可忽略 |
7. 总结
7.1 核心要点回顾
@Builder解决的是什么:省去手写建造者模式的样板代码,一行注解替代上百行 Builder 类- 默认值一定要加
@Builder.Default:否则字段初始值会被 Java 零值覆盖,这是最高频的坑 - 集合用
@Singular:让集合字段的赋值像普通字段一样自然,同时保留一次性覆盖的能力 - 继承用
@SuperBuilder:父子类全部加,否则父类字段丢失 - 框架集成加两兄弟 :
@NoArgsConstructor+@AllArgsConstructor,解决 Jackson / Hibernate 反序列化问题 - Entity 谨慎用:JPA 代理类 + Builder 有潜在冲突,建议 Entity 保持简单,复杂构造下沉到 DTO 层
7.2 适用边界
- ✅ DTO、VO、Request、Response 对象------最推荐的使用场景
- ✅ 配置类、参数对象(含大量可选参数和默认值)
- ✅ 需要不可变对象的场景(配合
final字段 +@Builder在构造器上) - ⚠️ JPA Entity------能用但有坑,见 6.3 节
- ❌ 1-2 字段的简单类------没必要引入 Builder 的复杂度
参考
- Lombok @Builder 官方文档
- Lombok @SuperBuilder 官方文档
- 详解Lombok中的@Builder用法
- @Builder注解详解:巧妙避开常见的陷阱-腾讯云开发者社区-腾讯云