核心概念
- Domain(领域模型)/实体(Entity) :通常映射到数据库表(JPA 中的
@Entity,MyBatis 中的 POJO)。包含数据 + 业务逻辑(通常只是数据,带有贫血模型,但在更广泛的意义上,它代表业务实体)。 - DTO(数据传输对象) :为传输而设计的普通数据容器(API 请求/响应、RPC 调用)。无业务逻辑。专为特定用例(如 UI 视图)量身定制。
1. Domain 和 DTO 的异同
| 对比维度 | Domain(领域对象 / PO / Entity) | DTO(数据传输对象) |
|---|---|---|
| 核心职责 | 持久化:负责与数据库表结构一一映射(ORM)。 | 传输:负责在"后端"与"前端(或第三方接口)"之间传递数据。 |
| 包含属性 | 全量字段 :包含数据库表中所有的列(比如:id, name, password, salt, create_time, update_time)。 |
部分字段 :只包含前端当前页面需要 的字段(比如:只返回 id 和 name,不要 password)。 |
| 包含逻辑 | 可以有业务逻辑:可以包含一些针对该实体类的校验方法或业务计算方法。 | 纯数据载体 :只应该有字段和 getter/setter,绝对不包含任何业务逻辑。 |
| 敏感信息 | 包含敏感信息:直接包含数据库里的密码、加密盐值等。 | 过滤敏感信息:严禁包含密码、身份证号等不该暴露给前端的数据。 |
2. 为什么需要分两个?
如果偷懒不分层,直接把 Domain 对象返回给前端(Controller 层直接返回 Entity),你会遇到以下 3 个致命问题:
- ① 安全问题(最严重) :你的
Domain对象里一定有password(密码密文)和salt(盐值)。如果你直接把 Domain 传给前端,这些敏感数据就会被接口泄露出去。而 DTO 可以只挑出username和avatar返回。 - ② 接口稳定性(防破环) :数据库表结构经常变动(比如加个字段、改个字段名)。如果你把 Domain 当接口返回对象,只要数据库加一个字段,所有调用你接口的前端 App 都得跟着改代码 (因为序列化后多了一个不需要的字段)。而有了 DTO,你可以通过
@JsonProperty或手动set/get组装,保证接口字段永远不变,哪怕数据库翻天覆地,前端也无需升级。 - ③ 结构聚合(性能与便捷性) :前端的一个页面往往需要多张表 的联合数据。比如前端需要"用户信息 + 订单总数 + 积分"。如果你只返回 Domain,前端需要调 3 个接口,发 3 次请求。但你可以在 Service 层查完 3 张表后,组装成一个完整的 DTO (比如
UserProfileDTO),一次返回给前端,大幅减少网络开销。
3. 一个生动的"外卖"类比
- Domain 就像中央厨房的后厨食材库(包含整只鸡、未处理的生肉、秘制酱料配方)。这些东西很全,但绝不能直接端给客人。
- DTO 就像端上餐桌的精美菜品(比如一盘宫保鸡丁)。它只是食材库的一部分组合,经过加工(业务处理),去掉了不能吃的部分(敏感信息),摆盘精致(结构符合前端展示需求)。
💡 实践中的归属划分
- DTO 的组装逻辑 写在 Service 层 (业务层),由 Service 从多个 Mapper 查数据,然后
new UserDTO(),往里set值,再返回给 Controller。 - Controller 层 只接收 Service 返回的 DTO,直接通过
@ResponseBody转为 JSON 返回给前端,Controller 绝不直接返回 Domain。
总结一句话:Domain 面向数据库(对内),DTO 面向用户(对外)。 分开了,后端代码可以随便重构优化,前端永远不知情,这才是正规军写法的精髓。