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 仓库。