Java Lombok @Builder 注解

你还在手写上百行 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 方式有三个硬伤:

  1. 无法创建不可变对象 :字段必须去掉 final,对象可以被任意修改
  2. 对象状态不完整:构造和赋值分离,中间状态可能被其他线程读取
  3. 缺乏语义:看不出哪些字段是必须的、哪些是可选的

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()(无法识别单数,保持不变)

支持的集合类型覆盖了日常需求:ListSetMapSortedSetSortedMapNavigableSetNavigableMap,以及 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 @Builderbuild() 方法 编译错误(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 核心要点回顾

  1. @Builder解决的是什么:省去手写建造者模式的样板代码,一行注解替代上百行 Builder 类
  2. 默认值一定要加 @Builder.Default:否则字段初始值会被 Java 零值覆盖,这是最高频的坑
  3. 集合用 @Singular:让集合字段的赋值像普通字段一样自然,同时保留一次性覆盖的能力
  4. 继承用 @SuperBuilder:父子类全部加,否则父类字段丢失
  5. 框架集成加两兄弟@NoArgsConstructor + @AllArgsConstructor,解决 Jackson / Hibernate 反序列化问题
  6. Entity 谨慎用:JPA 代理类 + Builder 有潜在冲突,建议 Entity 保持简单,复杂构造下沉到 DTO 层

7.2 适用边界

  • ✅ DTO、VO、Request、Response 对象------最推荐的使用场景
  • ✅ 配置类、参数对象(含大量可选参数和默认值)
  • ✅ 需要不可变对象的场景(配合 final 字段 + @Builder 在构造器上)
  • ⚠️ JPA Entity------能用但有坑,见 6.3 节
  • ❌ 1-2 字段的简单类------没必要引入 Builder 的复杂度

参考