找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

328

积分

0

好友

44

主题
发表于 21 小时前 | 查看: 6| 回复: 0

面对字段众多的实体类,你是否也曾发出过这样的感叹:“这个User对象太复杂了,new完又要setset完又要验证,能不能简洁点?”于是,很多开发者想到了使用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; 这行代码仅仅复制了对象引用,backupuser指向堆内存中的同一个实例。所谓的“修复” User backup = user.toBuilder().build(); 对于基本类型和String是有效的,但对于Date等引用类型字段,仍然是浅拷贝,两个对象的Date字段引用的是同一个Date对象。

四、踩坑二:JSON序列化异常

事故场景:当需要通过Spring Boot的@RestControllerUser对象返回给前端时。

@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变成了时间戳)。

排查根源

  1. 尽管类上添加了@Getter注解,但Lombok@Builder的工作机制与Jackson序列化器可能存在微妙的不兼容,尤其是在配合toBuilder()等复杂场景下。
  2. 更隐蔽的问题是,如果对象中存在循环引用(例如User中有Order列表,Order中又有关联的User),使用生成的toBuilder()进行拷贝极易导致栈溢出错误。

五、解决方案:手写Builder,获得完全控制权

鉴于Lombok @Builder在复杂场景下的局限性,最终决定放弃注解,采用手写Builder模式。这虽然增加了代码量,但带来了对构建过程的完全控制。

手写Builder的核心要点:

  1. 私有化构造器:强制通过Builder创建对象。
  2. 静态内部类:作为Builder,持有与目标类相同的字段。
  3. 链式SetterBuilder的方法返回自身,支持链式调用。
  4. 深拷贝处理:在Builder的构造器和方法中,对Date等可变引用类型显式创建新对象。
  5. 参数验证:在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对象构建的深度实践,可以总结出建造者模式的几点核心使用原则:

  1. 本质是简化创建:其核心价值在于简化多参数、尤其是可选参数众多且需要验证的复杂对象的创建过程。
  2. 工具选择因场景而异:Lombok @Builder是提升简单场景开发效率的利器;而手写Builder则是处理复杂场景、追求稳定可控的终极手段。
  3. 深拷贝不可忽视:在涉及状态复制、缓存、并发访问的场景下,对Date、集合等可变对象的深拷贝是必须考虑的安全措施。
  4. 验证前置保障安全:在build()方法中完成参数校验,能保证交付的对象从一开始就处于合法、一致的状态。

最终结论:建造者模式是管理复杂对象创建的优秀设计模式。Lombok @Builder提供了快捷实现,适合大多数简单场景;但在面对深拷贝、复杂校验等高阶需求时,手写Builder虽然繁琐,却提供了无可替代的可靠性与灵活性。根据实际项目复杂度做出合适的选择,才是资深开发者的体现。




上一篇:Java线程池源码深度剖析与高并发场景实战
下一篇:蓝队溯源分析实战:一次完整的Web入侵事件复盘
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-6 23:54 , Processed in 0.115157 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表