1. 简介

在本篇文章中,我们将探讨如何在使用 Lombok 的 Builder 模式时为属性设置默认值。

如果你对 Lombok 还不熟悉,建议先阅读我们的 Lombok 入门指南

2. 依赖配置

我们将在本教程中使用 Lombok,因此只需要添加以下依赖项:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency>

3. 使用 Lombok Builder 的 POJO 类

首先来看一下 Lombok 是如何帮助我们省去手动实现 Builder 模式的样板代码的。

以一个简单的 POJO 类为例:

public class Pojo {
    private String name;
    private boolean original;
}

为了让这个类可用,我们通常需要 getter 方法。如果用于 ORM 框架(如 JPA),还需要提供默认构造函数。

此外,我们希望该类支持 Builder 模式。使用 Lombok,只需几个注解即可搞定:

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Pojo {
    private String name;
    private boolean original;
}

4. 明确预期行为

我们通过单元测试的形式来定义我们期望的行为。

第一个也是最基本的要求是:通过 Builder 构建对象后,默认值应该生效:

@Test
public void givenBuilderWithDefaultValue_ThanDefaultValueIsPresent() {
    Pojo build = Pojo.builder()
        .build();
    Assert.assertEquals("foo", build.getName());
    Assert.assertTrue(build.isOriginal());
}

✅ 当然,这个测试会失败,因为 @Builder 注解不会自动填充默认值。稍后我们会解决这个问题。

如果配合 ORM 使用,它通常依赖于默认构造函数。所以我们期望默认构造函数的行为与 Builder 保持一致:

@Test
public void givenBuilderWithDefaultValue_ThanNoArgsWorksAlso() {
    Pojo build = Pojo.builder()
        .build();
    Pojo pojo = new Pojo();
    Assert.assertEquals(build.getName(), pojo.getName());
    Assert.assertTrue(build.isOriginal() == pojo.isOriginal());
}

⚠️ 此时这个测试是通过的。

接下来我们看看如何让两个测试都通过。

5. Lombok 的 @Builder.Default 注解

从 Lombok v1.16.16 开始,我们可以使用 @Builder 的内部注解:

// class annotations as before
public class Pojo {
    @Builder.Default
    private String name = "foo";
    @Builder.Default
    private boolean original = true;
}

这种方式简单且直观。Builder 会使用这些默认值,使第一个测试通过。

❌ 但注意:如果你使用的是低于 1.18.2 版本的 Lombok,无参构造函数不会包含默认值,第二个测试会失败。

✅ 幸运的是,Lombok 团队在 1.18.2 版本修复了这个问题。由于本教程使用的是更高版本,所以两个测试都能通过。

6. 手动初始化 Builder

我们也可以通过手动实现一个最小化的 Builder 来让两个测试通过:

// class annotations as before
public class Pojo {
    private String name = "foo";
    private boolean original = true;

    public static class PojoBuilder {
        private String name = "foo";
        private boolean original = true;
    }
}

✅ 这样做可以让两个测试都通过。

⚠️ 不过代价是明显的——代码重复。对于字段较多的 POJO,维护双重初始化容易出错。

而且还有一个坑:如果你在 IDE 中重命名类名,静态内部类不会被同步重命名,导致 Lombok 找不到 Builder 类,从而引发编译错误。

为了避免这个问题,可以显式指定 Builder 类名:

// class annotations as before
@Builder(builderClassName = "PojoBuilder")
public class Pojo {
    private String name = "foo";
    private boolean original = true;

    public static class PojoBuilder {
        private String name = "foo";
        private boolean original = true;
    }
}

7. 使用 toBuilder 特性

@Builder 还支持从已有对象实例生成一个新的 Builder 实例。默认情况下这个特性是关闭的,需要显式启用:

// class annotations as before
@Builder(toBuilder = true)
public class Pojo {
    private String name = "foo";
    private boolean original = true;
}

✅ 这样就可以避免双重初始化的问题。

⚠️ 代价是:我们必须先创建一个实例才能调用 toBuilder() 因此也需要修改测试代码:

@Test
public void givenBuilderWithDefaultValue_ThanDefaultValueIsPresent() {
    Pojo build =  new Pojo().toBuilder()
        .build();
    Assert.assertEquals("foo", build.getName());
    Assert.assertTrue(build.isOriginal());
}

@Test
public void givenBuilderWithDefaultValue_ThanNoArgsWorksAlso() {
    Pojo build = new Pojo().toBuilder()
        .build();
    Pojo pojo = new Pojo();
    Assert.assertEquals(build.getName(), pojo.getName());
    Assert.assertTrue(build.isOriginal() == pojo.isOriginal());
}

✅ 同样,两个测试都会通过,确保默认构造函数和 Builder 行为一致。

8. 小结

本文介绍了几种为 Lombok Builder 设置默认值的方法。

  • @Builder.Default 是最常用也最推荐的方式,但要注意版本兼容性问题。
  • 手动初始化虽然灵活,但容易出错且冗余。
  • toBuilder 提供了一种折中方案,适合需要基于已有对象创建 Builder 的场景。

选择哪种方式,取决于你的具体需求和项目环境。

📘 完整代码示例请参考 GitHub 仓库


原始标题:Lombok Builder with Default Value