面对字段众多的实体类,你是否也曾发出过这样的感叹:“这个User对象太复杂了,new完又要set,set完又要验证,能不能简洁点?”于是,很多开发者想到了使用Lombok的@Builder注解,一行代码似乎就能解决所有问题!
然而,在实际项目中,这个看似万能的@Builder却让我踩了两个大坑:深拷贝失败导致数据错乱、JSON序列化意外报错。本文将结合一个拥有20个字段的User对象案例,分享从踩坑到解决的全过程。
一、场景:拥有20个字段的User对象
首先明确业务场景,我们的User对象字段非常多,结构如下:
@Entity
@Table(name = "user")
public class User {
private Long id;
private String username;
private String password;
private String nickname;
private String email;
private String phone;
private Integer age;
private Integer gender;
private String avatar;
private String address;
private String city;
private String province;
private String country;
private Date birthday;
private String idCard;
private String realName;
private Integer status;
private Date createTime;
private Date updateTime;
private String createBy;
private String updateBy;
private String remark;
// 省略20个getter和setter方法...
}
使用传统方式创建这样一个对象的代码非常冗长:
User user = new User();
user.setUsername("zhangsan");
user.setPassword("123456");
user.setNickname("张三");
// ... 还需要继续set剩下的17个字段
传统方式的痛点:
- 代码冗长:动辄数十行,容易遗漏设置某些字段。
- 可读性差:无法直观看出哪些字段是必须的。
- 易出错:可能因拼写错误而
set错字段。
- 缺乏验证:对象可能在非法状态下被创建和持久化。
二、初次尝试:拥抱Lombok @Builder
为了解决上述问题,第一反应便是使用Lombok的@Builder注解,它堪称Java对象创建的“语法糖”。
@Entity
@Table(name = "user")
@Getter
@Setter
@Builder // 关键注解
@NoArgsConstructor
@AllArgsConstructor
public class User {
// ... 所有字段定义同上
}
创建对象的代码瞬间变得优雅:
User user = User.builder()
.username("zhangsan")
.password("123456")
.nickname("张三")
.email("zhangsan@example.com")
// ... 链式设置其他字段
.build();
优点显而易见:
- 代码简洁:链式调用,一目了然。
- 不易遗漏:清晰的字段列表。
- 语义明确:
build()方法标志着对象创建完成。
当时的感觉:Lombok真香!一行注解解决所有烦恼。
三、踩坑一:深拷贝失败,引发数据错乱
事故场景:在用户注册逻辑中,需要创建User对象并复制一份作为数据备份。
@Service
public class UserService {
public void register(UserRegisterDTO dto) {
// 1. 使用Builder创建用户对象
User user = User.builder()
.username(dto.getUsername())
.password(dto.getPassword())
// ... 设置其他字段
.build();
// 2. 保存用户
userMapper.insert(user);
// 3. 创建备份(错误做法)
User backup = user; // 这只是引用复制!
backup.setStatus(0); // 修改备份状态
// 4. 保存备份
userBackupMapper.insert(backup); // 问题:原user对象的状态也被改了!
}
}
问题现象:用户表和备份表的数据状态意外地变成了相同值,修改备份对象导致了原对象一同被修改。
错误分析:User backup = user; 这行代码仅仅复制了对象引用,backup和user指向堆内存中的同一个实例。所谓的“修复” User backup = user.toBuilder().build(); 对于基本类型和String是有效的,但对于Date等引用类型字段,仍然是浅拷贝,两个对象的Date字段引用的是同一个Date对象。
四、踩坑二:JSON序列化异常
事故场景:当需要通过Spring Boot的@RestController将User对象返回给前端时。
@RestController
public class UserController {
@GetMapping("/api/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
User user = userMapper.selectById(id);
return Result.success(user); // 序列化给前端
}
}
问题现象:前端接收到的JSON数据中,部分字段为null或类型不正确(如Date变成了时间戳)。
排查根源:
- 尽管类上添加了
@Getter注解,但Lombok@Builder的工作机制与Jackson序列化器可能存在微妙的不兼容,尤其是在配合toBuilder()等复杂场景下。
- 更隐蔽的问题是,如果对象中存在循环引用(例如
User中有Order列表,Order中又有关联的User),使用生成的toBuilder()进行拷贝极易导致栈溢出错误。
五、解决方案:手写Builder,获得完全控制权
鉴于Lombok @Builder在复杂场景下的局限性,最终决定放弃注解,采用手写Builder模式。这虽然增加了代码量,但带来了对构建过程的完全控制。
手写Builder的核心要点:
- 私有化构造器:强制通过
Builder创建对象。
- 静态内部类:作为
Builder,持有与目标类相同的字段。
- 链式Setter:
Builder的方法返回自身,支持链式调用。
- 深拷贝处理:在
Builder的构造器和方法中,对Date等可变引用类型显式创建新对象。
- 参数验证:在
build()方法中集中进行业务规则校验。
以下是关键代码片段示例:
public class User {
private Date birthday;
// ... 其他字段
// 私有构造器,接收Builder
private User(Builder builder) {
// 对引用类型进行深拷贝
this.birthday = builder.birthday != null ? new Date(builder.birthday.getTime()) : null;
// ... 赋值其他字段
}
public static Builder builder() {
return new Builder();
}
// 提供深拷贝的toBuilder方法
public Builder toBuilder() {
return new Builder(this);
}
public static class Builder {
private Date birthday;
// ... Builder持有相同字段
// 用于深拷贝的构造器
public Builder(User user) {
this.birthday = user.birthday != null ? new Date(user.birthday.getTime()) : null;
// ... 复制其他字段
}
// Builder的链式方法也进行深拷贝保护
public Builder birthday(Date birthday) {
this.birthday = (birthday != null) ? new Date(birthday.getTime()) : null;
return this;
}
public User build() {
// 集中进行参数验证
if (username == null || username.isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
// ... 其他验证
return new User(this);
}
}
// ... 标准的getter方法(同样可做防御性拷贝)
}
六、手写Builder的应用
安全创建对象:
User user = User.builder()
.username("lisi")
.password("encodedPwd")
.birthday(new Date()) // 内部已深拷贝
.build(); // 自动触发验证
实现真正的深拷贝:
User original = userMapper.selectById(1L);
User backup = original.toBuilder() // 深拷贝
.status(0)
.build();
userBackupMapper.insert(backup); // 完全独立的对象
JSON序列化:由于拥有完整的getter方法,手写的User类与Jackson等序列化框架可以完美协作,不再有字段丢失或类型错误的问题。
七、Lombok @Builder 与 手写Builder 对比与选型
| 特性 |
Lombok @Builder |
手写 Builder |
| 代码量 |
极简(1行注解) |
繁多(上百行) |
| 开发效率 |
极高 |
低 |
| 可定制性 |
低(依赖注解参数) |
极高(完全控制) |
| 深拷贝 |
不支持(浅拷贝) |
支持(可精细控制) |
| 参数验证 |
有限(通过@Builder.Default等) |
强大(可在build()中实现复杂逻辑) |
| 维护成本 |
低(但依赖Lombok版本) |
高 |
选型建议:
-
使用 Lombok @Builder 的场景:
- 对象字段较少(例如少于10个)。
- 不存在可变引用类型字段,或不需要深拷贝。
- 参数验证逻辑简单。
- 团队已熟悉并统一使用Lombok。
-
推荐手写 Builder 的场景:
- 对象字段众多,构造逻辑复杂。
- 必须确保深拷贝(例如缓存、备份场景)。
- 需要执行复杂的业务参数校验。
- 希望代码生成过程完全透明,便于调试和维护。
折中推荐方案:
可以结合两者优点,使用Lombok生成Getter/Setter/ToString等样板代码,仅手动编写复杂的Builder部分。
@Getter // Lombok生成Getter
@Setter // Lombok生成Setter
public class User {
private Long id;
private String username;
// ... 其他字段
// 手动编写定制化的Builder
public static Builder builder() { return new Builder(); }
public static class Builder {
// ... 手写Builder实现,包含深拷贝和验证
}
}
总结:建造者模式的核心原则
通过这次对User对象构建的深度实践,可以总结出建造者模式的几点核心使用原则:
- 本质是简化创建:其核心价值在于简化多参数、尤其是可选参数众多且需要验证的复杂对象的创建过程。
- 工具选择因场景而异:Lombok
@Builder是提升简单场景开发效率的利器;而手写Builder则是处理复杂场景、追求稳定可控的终极手段。
- 深拷贝不可忽视:在涉及状态复制、缓存、并发访问的场景下,对
Date、集合等可变对象的深拷贝是必须考虑的安全措施。
- 验证前置保障安全:在
build()方法中完成参数校验,能保证交付的对象从一开始就处于合法、一致的状态。
最终结论:建造者模式是管理复杂对象创建的优秀设计模式。Lombok @Builder提供了快捷实现,适合大多数简单场景;但在面对深拷贝、复杂校验等高阶需求时,手写Builder虽然繁琐,却提供了无可替代的可靠性与灵活性。根据实际项目复杂度做出合适的选择,才是资深开发者的体现。