理清Java后端VO、DTO、PO、BO、DO,告别过度分层与概念混淆
前言
很多Java开发新手、甚至3年以内工程师,都会混淆 VO、DTO、PO、BO、DO 五种数据对象的使用场景。
一、先看结论:一张表看懂所有
| 类型 | 全称 | 一句话作用 | 用在哪一层 |
|---|---|---|---|
| PO/Entity | Persistent Object | 和数据库表一一对应 | Mapper层(DAO层) |
| DTO | Data Transfer Object | 接收前端传过来的参数 | Controller层接收参数 |
| VO | View Object | 返回给前端展示的数据 | Controller层返回结果 |
| BO | Business Object | Service层内部业务流转 | Service层内部 |
| DO | Domain Object | 包含业务逻辑的领域对象 | Domain层(DDD) |
核心原则:各司其职,互不干扰,但也不必为了分层而分层。
二、为什么要分?直接用Entity不行吗?
2.1 直接用的后果
| 问题 | 具体表现 |
|---|---|
| 字段暴露 | Entity里有delete_flag、create_user、update_time等内部字段,不该让前端知道 |
| 敏感信息泄露 | 身份证号、手机号等敏感字段直接返回给前端,没有任何脱敏处理 |
| 参数校验混乱 | 新增时需要校验手机号,修改时不需要,但用的是同一个对象,校验逻辑互相干扰 |
| 多表关联难处理 | Entity只能映射单表,但前端需要显示单位名称、项目部名称等关联信息 |
| 改动影响面大 | 数据库表加了一个字段,所有接口的返回值都变了,前端可能报错 |
| 接口语义不清 | 接口参数类型是Entity,有20个字段,前端根本不知道哪些是必传的 |
2.2 但分层也不是越多越好
| 问题 | 具体表现 |
|---|---|
| 过度分层 | 一个简单接口,DTO→BO→Entity→VO转了4层,代码量翻3倍 |
| 概念混乱 | BO和DO分不清,有的人把DO当PO用,有的人把BO当VO用 |
| 转换成本高 | 每个转换都要写大量的set/get,容易出错,维护成本高 |
三、各类型详解
3.1 PO / Entity:数据库的镜子
定义: 和数据库表一一对应的Java类,表里有什么字段,这个类就有什么属性。
package com.dhcad.zhaz.person.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_zhgd_person_info")
public class PersonInfo {
@TableId("id")
private String id;
private String personName;
private String idCard;
private String phone;
private String gender;
private String nation;
private Integer age;
private String orgId;
private String projectDepartmentId;
private String facePhoto;
private Integer deleteFlag;
private String createUser;
private LocalDateTime createTime;
private String updateUser;
private LocalDateTime updateTime;
}
特点:
-
和数据库表字段完全一致
-
不包含任何业务逻辑,纯数据载体
-
只在Mapper层和Service层使用
-
绝不直接返回给前端
3.2 DTO:前端的"接盘侠"
定义: 专门用来接收前端请求参数的。前端传什么字段,我就定义什么字段。
package com.dhcad.zhaz.person.dto;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@Data
public class PersonAddDTO {
@NotBlank(message = "姓名不能为空")
private String personName;
@NotBlank(message = "身份证号不能为空")
@Pattern(regexp = "(^\\d{15}$)|(^\\d{18}$)|(^\\d{17}(\\d|X|x)$)",
message = "身份证号格式不正确")
private String idCard;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
private String gender;
private String nation;
private Integer age;
private MultipartFile facePhotoFile;
}
特点:
-
按需定义,只包含前端需要传的字段
-
可以加校验注解
-
可以包含
MultipartFile等PO里没有的类型
3.3 VO:前端的"定制皮肤"
定义: 专门用来返回给前端展示的。前端需要展示哪些字段,我就定义哪些字段。
package com.dhcad.zhaz.person.vo;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang3.StringUtils;
import java.time.LocalDateTime;
@Data
public class PersonInfoVO {
private String id;
private String personName;
private String phone; // getter里脱敏
private String idCard; // getter里脱敏
private String gender;
private String nation;
private String orgName; // 关联查询来的
private String projectDepartmentName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
public String getPhone() {
if (StringUtils.isBlank(phone) || phone.length() < 11) {
return phone;
}
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
public String getIdCard() {
if (StringUtils.isBlank(idCard) || idCard.length() < 18) {
return idCard;
}
return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
}
}
特点:
-
按需展示,只包含前端需要展示的字段
-
可以做脱敏处理
-
可以做格式化
-
可以包含多表关联的字段
3.4 BO:业务层的"工具人"
定义: Service层内部使用的业务对象,用于封装复杂的业务数据。
package com.dhcad.zhaz.person.bo;
import lombok.Data;
@Data
public class CompanyJobStatBO {
private String companyName;
private String workType;
private Integer count;
}
特点:
-
只在Service层内部使用
-
不直接暴露给前端
-
不直接操作数据库
-
用于多个PO的组合或中间数据封装
什么时候用BO:
-
需要组合多个PO的数据时
-
业务逻辑有复杂的中间状态需要封装时
-
不想让这些中间数据污染DTO或VO时
什么时候不用BO:
-
简单的CRUD,Service只是调用Mapper,不需要额外封装
-
Entity和VO字段基本一致,可以直接转
3.5 DO:包含业务逻辑的领域对象
定义: Domain Object,不只包含数据,还包含业务行为的领域对象。
package com.dhcad.zhaz.person.domain;
import lombok.Data;
@Data
public class PersonDO {
private String id;
private String personName;
private String idCard;
private Integer age;
/**
* 判断是否成年 - 包含业务逻辑
*/
public boolean isAdult() {
return age != null && age >= 18;
}
/**
* 身份证号脱敏 - 包含业务逻辑
*/
public String getIdCardMasked() {
if (idCard == null || idCard.length() < 18) {
return idCard;
}
return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
}
}
特点:
-
包含业务逻辑,不只是数据载体(充血模型)
-
主要在DDD(领域驱动设计)架构中使用
四、核心问题:BO和DO到底有什么区别?
这是很多开发者最困惑的地方。两者都带"O",看起来都是业务相关的对象,到底有什么区别?
| 对比维度 | BO(Business Object) | DO(Domain Object) |
|---|---|---|
| 核心职责 | 封装业务数据的传输和组合 | 封装业务规则和行为 |
| 是否包含逻辑 | ❌ 不包含,纯数据载体 | ✅ 包含业务方法 |
| 使用场景 | Service层内部流转数据 | 承载核心业务规则 |
| 生命周期 | 方法级别,用完即弃 | 贯穿业务生命周期 |
| 架构风格 | 传统三层架构(Service重) | DDD领域驱动设计(Domain重) |
| 和数据的关系 | 组合多个PO的数据 | 一个DO通常对应一个聚合根 |
| 举例 | CompanyJobStatBO(统计中间数据) |
PersonDO.isAdult()(判断是否成年) |
一句话区别:
-
BO是"数据袋子":把几个PO的数据装在一起,方便在Service里传递
-
DO是"业务大脑":不仅装数据,还包含业务规则和行为
什么时候用BO,什么时候用DO?
| 场景 | 用什么 | 原因 |
|---|---|---|
| 传统三层架构(Controller-Service-Mapper) | BO | Service层做所有业务逻辑,BO只负责传数据 |
| DDD架构(领域模型驱动) | DO | 业务逻辑放在DO里,Service只做协调 |
| 一个简单的统计查询 | BO | 只需要装数据,不需要行为 |
| 复杂的计价/风控规则 | DO | 规则本身应该由领域对象自己判断 |
五、日常开发中的常见问题
5.1 过度分层(Over-Layering)
现象: 一个简单的CRUD接口,创造了4-5层对象。
// 问题代码:过度分层
UserDTO → UserBO → UserEntity → UserVO
// 每层都有转换,代码量翻3倍,维护成本暴增
后果:
-
代码量膨胀:每个转换都要写set/get
-
维护成本高:改一个字段要改4个类
-
新人看不懂:不知道为什么要分这么多层
正确做法:简单场景直接简化
// ✅ 简单的CRUD,直接用 Entity → VO 就够了
UserEntity → UserVO
// ✅ 接收参数,直接用 Entity 接(如果字段完全匹配)
public Result add(@RequestBody UserEntity entity)
// ✅ 或者用 DTO → Entity,不经过 BO
UserDTO → UserEntity → UserVO
5.2 分层混乱(Layering Confusion)
现象: 不按规范使用各类对象。
// ❌ 把Entity直接返回给前端
@GetMapping("/list")
public Result<List<PersonInfo>> list() {
return Result.success(personInfoMapper.selectList());
}
// 问题:delete_flag、create_user等内部字段全暴露了
// ❌ 用VO接收前端参数
@PostMapping("/add")
public Result add(@RequestBody PersonInfoVO vo) {
// 问题:VO里可能有脱敏逻辑,接收参数用VO语义不对
}
// ❌ 把DTO当VO用,返回给前端
@GetMapping("/detail")
public Result<PersonAddDTO> detail() {
// 问题:DTO是入参,不该作为出参
}
正确做法:按规范使用
| 用途 | 用什么 | 禁止用 |
|---|---|---|
| 接收参数 | DTO | VO、Entity |
| 返回数据 | VO | Entity、DTO |
| 操作数据库 | Entity/PO | VO、DTO |
| 业务内部流转 | BO(需要时) | VO、DTO |
5.3 BO的滥用
现象: 不管需不需要,Service都创建一个BO。
// ❌ 问题代码:不必要的BO
@Override
public UserVO getUser(String id) {
// 查询Entity
UserEntity entity = userMapper.selectById(id);
// 转成BO(毫无意义,因为只有一个PO)
UserBO bo = new UserBO();
bo.setId(entity.getId());
bo.setName(entity.getName());
// 再转成VO
UserVO vo = new UserVO();
vo.setId(bo.getId());
vo.setName(bo.getName());
return vo;
}
正确做法:没有复杂组装,直接转
// ✅ 直接转,不需要BO
@Override
public UserVO getUser(String id) {
UserEntity entity = userMapper.selectById(id);
return convertToVO(entity);
}
5.4 PO和Entity混用
有的项目叫XxxPO,有的叫XxxEntity,还有的叫XxxModel。建议统一命名规范,团队内部保持一致。
六、各层之间的流转与转换
6.1 完整数据流转图
前端请求 (JSON)
↓
┌─────────────────────────────────────────────────────┐
│ Controller 层 │
│ 1. 接收 DTO (入参) │
│ 2. 调用 Service │
│ 3. 返回 VO (出参) │
└─────────────────────────────────────────────────────┘
↓ ↑
┌─────────────────────────────────────────────────────┐
│ Service 层 │
│ 【简单场景】DTO → Entity → VO │
│ 【复杂场景】DTO → BO → Entity → BO → VO │
└─────────────────────────────────────────────────────┘
↓ ↑
┌─────────────────────────────────────────────────────┐
│ Mapper 层 │
│ Entity (PO) 作为参数和返回值 │
└─────────────────────────────────────────────────────┘
↓ ↑
数据库
6.2 转换代码示例
// ==================== Controller 层 ====================
@RestController
@RequestMapping("/api/person")
@RequiredArgsConstructor
public class PersonInfoController {
private final PersonInfoService personInfoService;
@PostMapping("/add")
public Result<PersonInfoVO> addPerson(@Valid @RequestBody PersonAddDTO dto) {
PersonInfoVO vo = personInfoService.addPerson(dto);
return Result.success(vo);
}
}
// ==================== Service 层 ====================
@Service
@RequiredArgsConstructor
@Slf4j
public class PersonInfoServiceImpl implements PersonInfoService {
private final PersonInfoMapper personInfoMapper;
@Override
@Transactional
public PersonInfoVO addPerson(PersonAddDTO dto) {
// DTO → Entity
PersonInfo entity = new PersonInfo();
entity.setId(UUID.randomUUID().toString().replace("-", ""));
entity.setPersonName(dto.getPersonName());
entity.setIdCard(dto.getIdCard());
entity.setPhone(dto.getPhone());
entity.setGender(dto.getGender());
entity.setDeleteFlag(0);
entity.setCreateTime(LocalDateTime.now());
// 操作数据库
personInfoMapper.insert(entity);
// Entity → VO
PersonInfoVO vo = new PersonInfoVO();
vo.setId(entity.getId());
vo.setPersonName(entity.getPersonName());
vo.setPhone(entity.getPhone()); // getter里自动脱敏
vo.setIdCard(entity.getIdCard());
vo.setCreateTime(entity.getCreateTime());
return vo;
}
}
七、行业标准流程与最佳实践
7.1 标准分层规范
| 层级 | 包名 | 类后缀 | 作用 | 是否必须 |
|---|---|---|---|---|
| Controller | controller |
无特殊后缀 | 接收请求、返回响应 | ✅ 必须 |
| Service接口 | service |
XxxService |
定义业务接口 | ✅ 必须 |
| ServiceImpl | service.impl |
XxxServiceImpl |
实现业务逻辑 | ✅ 必须 |
| Mapper/DAO | mapper 或 dao |
XxxMapper |
操作数据库 | ✅ 必须 |
| Entity/PO | entity 或 po |
XxxEntity / XxxPO |
数据库映射 | ✅ 必须 |
| DTO | dto |
XxxDTO / XxxAddDTO / XxxQueryDTO |
接收参数 | ⚠️ 按需 |
| VO | vo |
XxxVO / XxxListVO / XxxDetailVO |
返回数据 | ⚠️ 按需 |
| BO | bo |
XxxBO / XxxStatBO |
业务内部流转 | ⚠️ 按需 |
| DO | domain |
XxxDO |
领域模型 | ⚠️ DDD专用 |
7.2 决策树:什么时候用什么
开始
│
▼
┌─────────────────────┐
│ 这个类是干什么的? │
└─────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
映射数据库 接收参数 返回展示
│ │ │
▼ ▼ ▼
Entity/PO 是否和 是否和
│ Entity Entity
│ 一致? 一致?
│ ┌──┴──┐ ┌──┴──┐
│ 是 否 是 否
│ │ │ │ │
│ │ DTO │ VO
│ │ │
└──────────┴───────────────┘
│
▼
有复杂业务组装?
│
┌─────┴─────┐
是 否
│ │
BO 不需要
7.3 不同项目规模的分层建议
| 项目规模 | 推荐分层 | 说明 |
|---|---|---|
| 小型项目(个人/小团队) | Entity + VO | 直接返回Entity给前端,简单高效 |
| 中型项目(企业级应用) | Entity + DTO + VO | 入参用DTO,出参用VO,规范清晰 |
| 大型项目(微服务/分布式) | Entity + DTO + VO + BO | 需要BO处理复杂业务组装 |
| DDD项目 | PO + DTO + VO + DO + BO | DO承载业务逻辑,BO做数据组装 |
7.4 行业最佳实践
1. 命名规范统一
// 推荐写法
com.dhcad.zhaz.person.entity.PersonInfo // Entity
com.dhcad.zhaz.person.dto.PersonAddDTO // 新增DTO
com.dhcad.zhaz.person.dto.PersonQueryDTO // 查询DTO
com.dhcad.zhaz.person.vo.PersonInfoVO // 详情VO
com.dhcad.zhaz.person.vo.PersonListVO // 列表VO
com.dhcad.zhaz.person.bo.CompanyStatBO // 统计BO
2. 使用MapStruct自动转换(推荐)
@Mapper(componentModel = "spring")
public interface PersonConverter {
PersonInfo toEntity(PersonAddDTO dto);
PersonInfoVO toVO(PersonInfo entity);
@Mapping(source = "orgName", target = "companyName")
CompanyStatBO toBO(CompanyJobStat stat);
}
3. 使用Lombok减少冗余代码
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PersonAddDTO {
// 一行代码搞定getter/setter/构造器
private String personName;
}
4. 参数校验统一放在DTO层
@Data
public class PersonAddDTO {
@NotBlank(message = "姓名不能为空")
@Size(max = 50, message = "姓名不能超过50个字符")
private String personName;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}
八、完整实战:人员统计接口(逐层演示)
8.1 请求DTO
@Data
public class StatQueryDTO {
private String type; // day/week/month/year
private String dateRange; // 2025.06.06-2025.06.10
}
8.2 内部BO(Service层组装用)
@Data
public class CompanyJobStatBO {
private String companyName;
private String workType;
private Integer count;
}
8.3 响应VO
@Data
public class StatVO {
private List<String> labels; // X轴
private List<Long> inData; // Y轴-进入
private List<Long> outData; // Y轴-离开
}
8.4 Controller
@PostMapping("/stat/person")
public Result<StatVO> statPerson(@RequestBody StatQueryDTO dto) {
StatVO vo = personInfoService.statPerson(dto);
return Result.success(vo);
}
8.5 Service(完整转换逻辑)
@Override
public StatVO statPerson(StatQueryDTO dto) {
// 1. 解析参数
DateRange range = parseDateRange(dto.getDateRange());
// 2. 查询数据(得到PO列表)
List<PersonAccessRecord> records = accessRecordMapper.selectByTimeRange(range);
// 3. 组装成BO(内部流转)
List<CompanyJobStatBO> boList = buildCompanyJobStat(records);
// 4. BO → VO
StatVO vo = new StatVO();
vo.setLabels(boList.stream()
.map(CompanyJobStatBO::getCompanyName)
.collect(Collectors.toList()));
vo.setInData(boList.stream()
.map(CompanyJobStatBO::getCount)
.map(Long::valueOf)
.collect(Collectors.toList()));
return vo;
}
九、总结
| 类型 | 作用 | 何时使用 | 何时不使用 |
|---|---|---|---|
| PO/Entity | 映射数据库表 | 操作数据库时 | 与前端交互时 |
| DTO | 接收前端参数 | 接口入参 | 作为出参 |
| VO | 返回前端展示 | 接口出参 | 作为入参 |
| BO | 业务内部流转 | 多个PO需要组合时 | 简单CRUD |
| DO | 领域模型(含逻辑) | DDD架构 | 传统三层架构 |
核心原则:
-
PO管数据库,DTO管接收,VO管展示,BO管业务,DO管领域
-
各司其职,互不干扰
-
不要为了分层而分层,恰到好处才是关键
检查清单(Code Review时用):
-
接口入参用的是DTO而不是Entity?
-
接口出参用的是VO而不是Entity?
-
Entity里的敏感字段(如deleteFlag)没有暴露给前端?
-
BO只在Service内部使用,没有传到Controller?
-
没有创建多余的BO(简单场景直接转)?
-
没有把DTO当VO用?
-
项目中的命名规范统一?
希望这篇文章能帮你彻底搞懂这些概念,在实际项目中合理地使用它们,既不过度分层,也不分层混乱。
更多推荐




所有评论(0)