SpringBoot开发必懂:VO、DTO、BO、DO、PO到底怎么用?一篇吃透不踩坑
做SpringBoot开发也有几年了,刚入门的时候,被VO、DTO、BO、DO、PO这些缩写搞得头大。对着网上的资料看,要么说得太抽象,要么全是理论套话,实操起来还是分不清什么时候该用哪个,甚至会乱混用——比如把数据库字段直接返回给前端,或者把前端传过来的参数直接丢给Service层,到后面需求迭代,改一处代码牵一发而动全身,排查bug能查到怀疑人生。
其实这些对象没有那么玄乎,核心就是“分层解耦”,把不同层的职责拆分开,让代码更清晰、更好维护。今天就用最接地气的话,结合我实际开发中遇到的场景,跟大家聊聊这些常用对象的区别和使用方法,没有复杂的理论,全是实操干货,新手也能一看就懂,避开那些我踩过的坑。
先声明一句:不同公司、不同项目的规范可能略有差异,但核心逻辑是一致的。本文分享的是最通用、最易落地的用法,适合绝大多数SpringBoot单体项目和简单微服务项目,不用追求过度设计,实用就好。
先搞懂核心:为什么需要这么多“O”?
很多新手会问:直接用一个实体类贯穿所有层不行吗?比如定义一个User类,既有数据库字段,又有前端展示的字段,还有业务计算的字段,多简单。
我刚入门的时候也这么干过,结果踩了大坑。举个例子:有一个用户列表接口,前端需要展示用户的id、姓名、手机号(脱敏)、角色名称,而数据库里存的是id、name、phone、role_id、password、create_time等字段。如果直接把数据库对应的实体类(比如User)返回给前端,会把password(密码)、create_time(创建时间)这些前端用不上的字段也返回过去,既浪费带宽,又有安全隐患;更麻烦的是,前端需要手机号脱敏(比如138****1234),如果在数据库实体类里加脱敏逻辑,那这个类就既承担了“对应数据库”的职责,又承担了“前端展示”的职责,后续如果前端要改脱敏规则,或者数据库要加字段,都得动这个类,极易出错。
而VO、DTO、BO、DO、PO的出现,就是为了解决这个问题——让每个对象只做自己的事,各层之间通过这些对象传递数据,互不干扰。简单说:分层解耦,职责单一,后续维护更省心。
逐个拆解:每个“O”到底是什么?怎么用?
下面逐个说明,重点讲“含义+使用场景+实操示例”,结合SpringBoot常用的分层(Controller、Service、Dao),让大家知道在哪个层该用哪个对象。
1. PO(Persistent Object):持久化对象,和数据库表一一对应
核心定位:PO是数据库表的“镜像”,字段和数据库表的列完全一致,没有任何业务逻辑,只负责承载数据库中的数据,说白了就是“用来存数据、取数据的载体”。
使用场景:仅在Dao层(数据访问层)使用,比如MyBatis的Mapper接口、XML映射文件中,用来接收数据库查询结果,或者作为插入/更新数据库的参数。
实操注意:
- 字段名和数据库列名保持一致(或者通过@Column注解映射),不添加任何多余字段;
- 只提供getter、setter方法,不写任何业务逻辑(比如计算、判断);
- 不要在Controller、Service层直接使用PO,避免数据库字段泄露或业务逻辑入侵。
示例(以用户表user为例):
// PO类,和数据库user表一一对应
public class UserPO {
// 对应数据库user表的id列
private Long id;
// 对应数据库user表的name列
private String name;
// 对应数据库user表的phone列
private String phone;
// 对应数据库user表的password列(加密存储)
private String password;
// 对应数据库user表的role_id列
private Long roleId;
// 对应数据库user表的create_time列
private LocalDateTime createTime;
// 只有getter、setter,无其他业务逻辑
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
// 其他getter、setter省略...
}
2. DO(Domain Object):领域对象,等价于PO?
这里要特别说明:很多人会把DO和PO搞混,甚至认为两者是同一个东西——在绝大多数SpringBoot单体项目中,DO和PO确实可以混用,因为项目规模不大,不需要过度拆分。
严格来说,两者的区别在于:PO更侧重“持久化”(和数据库的映射),而DO更侧重“领域模型”(承载领域内的基础数据,可能包含极简单的领域逻辑)。但在实际开发中,除非是大型项目、领域模型复杂,否则不需要单独定义DO,直接用PO代替DO即可,避免冗余。
我的实操建议:中小规模SpringBoot项目,不用区分DO和PO,直接用PO作为持久化对象和领域基础对象,减少类的数量,降低维护成本。如果是大型项目,领域逻辑复杂,可以拆分DO和PO(比如DO包含部分领域逻辑,PO仅负责数据库映射)。
3. BO(Business Object):业务对象,承载业务逻辑
核心定位:BO是Service层(业务逻辑层)的核心对象,用来承载业务逻辑、进行业务计算,它的数据可以来自一个或多个PO(/DO),是“业务处理的载体”。
使用场景:仅在Service层内部使用,或者Service层之间传递数据,用来封装业务处理过程中的数据和逻辑,Controller层不直接操作BO。
实操注意:
- BO可以包含多个PO(/DO)的数据,比如“订单业务对象”可以包含订单PO、用户PO、商品PO的数据;
- BO中可以包含业务逻辑方法(比如计算订单总价、判断用户是否有权限);
- BO不直接和数据库交互,数据来自Dao层查询的PO,经过Service层处理后封装成BO。
示例(用户业务对象,结合用户PO和角色PO):
// BO类,承载用户相关业务逻辑
public class UserBO {
// 来自UserPO的基础数据
private Long userId;
private String userName;
private String phone;
// 来自RolePO的数据(关联查询得到)
private Long roleId;
private String roleName;
// 业务计算字段(非数据库字段)
private boolean isVip; // 是否为VIP用户
// 业务逻辑方法:判断是否为VIP用户(示例)
public void judgeVip(LocalDateTime registerTime) {
// 业务规则:注册时间超过3年即为VIP
this.isVip = registerTime.plusYears(3).isBefore(LocalDateTime.now());
}
// getter、setter省略...
}
使用场景示例:Service层查询用户信息时,先通过Dao层查询UserPO和RolePO,然后将两者的数据封装到UserBO中,调用judgeVip方法判断是否为VIP,再将BO用于后续业务处理。
4. DTO(Data Transfer Object):数据传输对象,用于层与层之间传递数据
核心定位:DTO是“数据传输的工具”,用来在不同层之间传递数据,解决各层对象字段不一致的问题,比如Controller层和Service层之间、Service层和Dao层之间,或者微服务之间的远程调用(比如Feign调用)。
这里要注意:DTO和BO的区别——BO侧重“业务逻辑”,DTO侧重“数据传输”,不含任何业务逻辑,只负责承载要传输的数据。
使用场景:
- 前端→Controller:前端传递的请求参数(比如新增用户时,前端传name、phone、roleId,不需要传id、createTime),用DTO接收;
- Service→Controller:Service层处理完业务后,将数据封装成DTO,返回给Controller层;
- 微服务之间调用:比如服务A调用服务B的接口,传递的数据用DTO封装,避免暴露自身的PO/BO。
实操注意:
- DTO的字段根据“传输需求”定义,不需要和PO/BO完全一致,只包含需要传输的字段;
- 不含任何业务逻辑,只提供getter、setter方法;
- 常用的有RequestDTO(前端请求参数)和ResponseDTO(后端响应数据),可以分开定义,更清晰。
示例(用户相关DTO):
// 1. RequestDTO:接收前端新增用户的请求参数
public class UserAddRequestDTO {
// 前端只需要传name、phone、roleId,其他字段由后端生成
private String name;
private String phone;
private Long roleId;
// getter、setter省略...
}
// 2. ResponseDTO:Service层处理完,返回给Controller的响应数据
public class UserResponseDTO {
// 前端需要展示的字段:id、name、phone(脱敏)、roleName
private Long id;
private String name;
private String phone; // 这里会在Service层处理成脱敏格式
private String roleName;
// getter、setter省略...
}
使用场景示例:前端提交新增用户的表单(name、phone、roleId),Controller层用UserAddRequestDTO接收,然后将DTO转换成BO/PO,交给Service层处理;Service层处理完成后,将PO/BO的数据转换成UserResponseDTO,返回给Controller层,再由Controller层返回给前端。
5. VO(View Object):视图对象,专门给前端展示用
核心定位:VO是“前端专属的数据载体”,字段完全对应前端页面需要展示的内容,比如数据脱敏、字段重命名、组合字段等,只为前端展示服务,不参与后端任何业务逻辑。
这里很多人会把VO和ResponseDTO搞混,其实两者的区别很简单:
- ResponseDTO:侧重“层与层之间的传输”,比如Service→Controller,可能包含前端不需要的字段(但实际开发中会尽量和前端需求对齐);
- VO:侧重“前端视图展示”,完全贴合前端页面的需求,比如前端需要“用户性别(男/女)”,而数据库存的是0/1,VO中就直接存“男/女”,不需要前端再做转换。
实操建议:中小规模项目中,VO和ResponseDTO可以混用,因为前端需求和传输需求基本一致,没必要单独定义两个类,增加冗余;如果前端需求和传输需求差异较大(比如前端需要大量组合字段、格式化字段),可以单独定义VO,由Controller层将ResponseDTO转换成VO后返回给前端。
示例(用户VO,贴合前端展示需求):
// VO类,专门给前端展示用
public class UserVO {
// 前端展示:用户ID(和数据库一致)
private Long userId;
// 前端展示:用户姓名(和数据库一致)
private String userName;
// 前端展示:脱敏手机号(数据库是完整手机号,VO中是脱敏后的数据)
private String phone; // 格式:138****1234
// 前端展示:角色名称(数据库存的是role_id,VO中是角色名称)
private String roleName;
// 前端展示:注册时间(格式化后,比如2023-01-01 12:00:00)
private String createTime;
// 前端展示:用户状态(数据库存0/1,VO中存“正常/禁用”)
private String status;
// getter、setter省略...
}
使用场景示例:Service层返回UserResponseDTO(包含完整phone、时间戳格式的createTime、0/1格式的status),Controller层将其转换成UserVO,处理phone脱敏、时间格式化、status转换,然后返回给前端,前端直接拿VO的字段展示即可,不需要做任何数据处理。
关键总结:各层对象的使用流程(SpringBoot实操版)
结合SpringBoot的分层架构(Controller→Service→Dao),给大家梳理一下这些对象的完整使用流程,看完就知道什么时候该用哪个了:
- \1. 前端发起请求:前端提交表单(比如新增用户),传递name、phone、roleId等参数,Controller层用【RequestDTO】接收;
- \2. Controller→Service:Controller层将RequestDTO转换成【BO】(或直接传递DTO,根据项目规模),调用Service层的业务方法;
- \3. Service→Dao:Service层将BO(或DTO)转换成【PO】,调用Dao层(Mapper)的方法,查询/操作数据库;
- \4. Dao→Service:Dao层查询数据库,返回【PO】给Service层;
- \5. Service层处理业务:Service层将PO的数据封装到【BO】中,执行业务逻辑(比如判断VIP、数据校验);
- \6. Service→Controller:Service层将BO转换成【ResponseDTO】(或【VO】),返回给Controller层;
- \7. Controller→前端:Controller层将ResponseDTO(或VO)返回给前端,前端接收数据并展示。
简化版(中小规模项目,最常用):前端请求→RequestDTO→Service→PO(操作数据库)→Service处理→ResponseDTO(VO)→前端展示。
避坑提醒:这些错误千万别犯!
结合我自己踩过的坑,给大家提几个实操中最容易犯的错误,避开这些,能少走很多弯路:
- 不要用PO直接返回给前端:会泄露数据库敏感字段(比如password、createTime),也会返回前端用不上的字段,增加带宽消耗;
- 不要在DTO/VO中写业务逻辑:DTO/VO只负责数据传输和展示,业务逻辑必须放在Service层(BO中),否则会导致职责混乱,后续难以维护;
- 不要过度设计:中小规模项目,不用强行拆分DO和PO、VO和ResponseDTO,避免类的数量过多,增加维护成本,实用优先;
- 字段命名要规范:PO的字段和数据库一致,DTO/VO的字段和前端一致,避免出现字段不匹配、歧义的问题(比如前端要userId,后端返回id,导致前端渲染失败);
- 转换逻辑要统一:DTO→PO、PO→BO、BO→VO的转换逻辑,建议单独写一个转换工具类(比如UserConvert),不要散落在Controller、Service层,方便后续修改。
最后总结
其实VO、DTO、BO、DO、PO这些对象,核心就是“分层解耦、职责单一”,没有那么复杂。记住一句话:什么层用什么对象,什么对象做什么事,不越界、不混用,代码就会清晰很多。
对于新手来说,不用一开始就死记硬背各个对象的定义,先在项目中尝试使用RequestDTO、ResponseDTO、PO这三个最常用的,熟悉之后,再根据项目规模,考虑是否拆分BO、VO、DO。实操多了,自然就懂了。
更多推荐

所有评论(0)