← 返回题目列表

Builder、JavaBean 和 DTO 在对象创建中如何取舍?

高频 中等 第 9 / 26 题 更新于 2026/08/01
建造者模式JavaBeanDTO对象创建

简化版

JavaBean 适合框架绑定和简单数据传输,Builder 适合创建字段多、可选项多、需要校验或希望不可变的业务对象,DTO 则主要承担跨层或跨进程的数据承载。取舍时看对象是否有业务不变量、是否需要不可变、是否由框架反射创建。

详细版

三者关注点不同:

  • JavaBean:无参构造器加 getter/setter,框架友好,但对象可能处于半初始化状态;
  • Builder:通过命名步骤收集参数,最后 build() 统一校验并创建成品对象;
  • DTO:用于接口、RPC、消息、持久化映射等数据传输,通常不承载复杂业务行为。

如果是 Controller 入参、JSON 反序列化对象、ORM 映射对象,JavaBean 或 record/DTO 往往更合适。如果是订单创建命令、HTTP 客户端配置、不可变值对象、复杂查询条件,Builder 更合适。

面试里不要把 Builder 说成“替代 setter 的高级写法”。真正判断标准是:对象创建是否需要清晰命名、统一校验、默认值、不可变交付和防止半成品泄漏。

完整版教学

一、JavaBean 的优势和天然风险

JavaBean 的典型结构是无参构造器加 setter。它对框架非常友好,因为 JSON 反序列化、表单绑定、ORM 映射都能先创建对象,再逐个填字段。

UserDTO dto = new UserDTO();
dto.setName("Tom");
dto.setAge(18);
dto.setEmail("tom@example.com");

它的问题也来自同一个机制:对象在 setter 没调用完之前就是半初始化状态。如果某段代码在 setName() 后、setEmail() 前拿到了对象,就可能看到不完整数据。对于纯数据传输对象,这个风险可控;对于有业务不变量的领域对象,就容易出问题。

记忆钩子:JavaBean 对框架友好,Builder 对业务不变量友好。

二、Builder 的核心收益不是少写代码

Builder 往往比 JavaBean 写更多代码,但它换来几个关键收益。第一,调用方能通过方法名看懂参数含义。第二,默认值集中在 Builder。第三,build() 是统一交付边界。第四,最终对象可以设计成不可变。

UserProfile profile = UserProfile.builder()
        .name("Tom")
        .age(18)
        .email("tom@example.com")
        .marketingOptIn(false)
        .build();

如果对象有 10 个字段,其中 4 个可选、3 个有默认值、2 个存在组合约束,Builder 会比 setter 更稳。因为 setter 只能表达“改了某个字段”,而 Builder 可以表达“收集参数后交付一个合法成品”。

三、DTO 的职责是传输,不是承载复杂业务

DTO 的重点是数据边界。它常出现在 API 入参出参、RPC 消息、MQ 消息、数据库投影、前后端交互中。DTO 可以用 JavaBean、record、构造器或 Builder 实现,但 DTO 本身不是一种创建型模式。

HTTP JSON -> CreateOrderRequestDTO -> Application Service -> Order.create(...)
数据库行 -> OrderPO -> Repository -> Order
Order -> OrderResponseDTO -> HTTP JSON

这条流程里,DTO 负责把外部数据带进来或带出去;业务对象负责表达业务规则。不要因为 DTO 字段多,就机械套 Builder。很多 DTO 需要被 Jackson、MyBatis、Spring MVC 等框架反射创建,JavaBean 反而更顺。

四、用数字判断哪种方式更合适

可以用字段数量和规则复杂度做粗略判断。比如一个对象只有 3 个字段且全部必填,构造器或 record 更直观。一个对象有 12 个字段,其中 8 个可选,且有 5 条校验规则,Builder 的收益明显更高。

场景字段数量规则复杂度推荐方式
坐标 Point2构造器/record
登录表单 DTO2-4JavaBean/record + 校验框架
HTTP 客户端配置8-15Builder
ORM 实体5-30JavaBean 或框架约定
领域值对象3-10中高构造器/静态工厂/Builder

这个表不是绝对规则,但能帮助面试回答从“喜好”变成“基于约束的选择”。

五、半初始化对象为什么是关键分界线

JavaBean 的 setter 允许对象被逐步修改,这意味着对象可能短暂处于不合法状态。比如 startTime 必须小于 endTime,用 setter 时很难保证任何时刻都满足。

Period period = new Period();
period.setStartTime(100);
period.setEndTime(50); // 此时对象已经处于非法状态

Builder 可以先收集两个字段,最后统一判断:

Period period = Period.builder()
        .startTime(100)
        .endTime(50)
        .build(); // 在这里拒绝

关键区别在于:JavaBean 先暴露对象再慢慢补齐,Builder 先收集参数再交付对象。业务对象越强调不变量,越应该避免半初始化状态泄漏。

六、框架绑定场景为什么常保留 JavaBean

Spring MVC、Jackson、MyBatis 等框架通常需要无参构造器、setter、字段访问或反射机制。虽然很多框架也支持构造器绑定和 Builder 反序列化,但配置复杂度会更高,团队一致性也要考虑。

例如一个请求 DTO:

public class CreateUserRequest {
    private String name;
    private Integer age;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
}

这类对象可以用校验注解处理输入合法性,然后在应用服务里转换成真正的领域对象:

User user = User.builder()
        .name(request.getName())
        .age(request.getAge())
        .build();

这样 DTO 和领域对象各守边界:DTO 适配外部协议,领域对象保护业务规则。

七、Builder 与 DTO 可以组合使用

Builder 不一定只属于领域对象。复杂响应 DTO、测试数据构造器、SDK 请求对象,也可以使用 Builder。关键是看调用方是否需要可读的构建体验和校验边界。

外部入参:
  JavaBean DTO + Bean Validation

内部领域:
  Builder / 静态工厂 + 不变量保护

外部出参:
  简单 DTO / record

测试数据:
  TestDataBuilder,提供合理默认值

测试数据 Builder 很常见。比如创建订单测试对象时,默认给一个合法订单,只在测试里覆盖关心字段,这比每个测试都手写 12 个字段更稳定。

八、常见误区与追问

  • 误区:Builder 一定比 JavaBean 更高级。 两者解决的问题不同,框架绑定场景 JavaBean 可能更合适。
  • 误区:DTO 字段多就应该用 Builder。 DTO 主要是传输结构,是否使用 Builder 要看构建复杂度和框架兼容。
  • 误区:JavaBean 一定不安全。 对简单传输对象可以接受,风险主要在有业务不变量的对象。
  • 追问:Controller 入参用 Builder 好不好? 通常不优先,除非框架和团队明确支持构造器或 Builder 绑定。
  • 追问:领域对象为什么不建议暴露 setter? setter 会让对象创建后继续被任意修改,不变量难以长期保证。
  • 追问:Builder 和 Bean Validation 怎么配合? 外部 DTO 用 Bean Validation 做输入校验,内部 Builder 做业务对象不变量校验。
  • 追问:record 能不能替代 Builder? 字段少、全部必填时 record 很好;字段多、可选项多、默认值多时 Builder 更清晰。

九、加强记忆

取舍时抓住三个问题:谁创建它、它有没有业务不变量、创建后能不能被随便改。框架创建的传输对象偏 JavaBean/DTO,业务规则强的成品对象偏 Builder 或静态工厂;不要为了“看起来像设计模式”把所有对象都改成 Builder。