DTO、VO、PO、BO 到底该怎么区分?

🌈个人主页 :一条泥憨鱼 (欢迎各位大佬莅临)

🎬精选专栏:数据结构与算法Java,AI与Agent

前言:

刚学 Java 那会儿,我对着项目里的 UserDTO、UserVO、UserPO、UserBO 看了半天------字段几乎一样,换来换去不嫌烦吗?

后来踩了几次坑才明白,不是闲得慌,是每一层有每一层的风险。一个典型的例子:有人直接把 PO 返回给前端,结果把加密后的密码也带出去了。虽然一般人解不开,但这本身就说明设计有问题。

四个对象,四种场景

PO(Persistent Object,持久化对象) --- 就是数据库那张表

你用 MyBatis 或 JPA,查出来的数据映射到一个对象上,这个对象就是 PO。表里有什么列,它就有什么字段,一个不多一个不少。

java 复制代码
@TableName("user")
public class UserPO {
    private Long id;
    private String username;
    private String password;      // 数据库里有,但绝不能往外传
    private Integer status;       // 0 或 1,存原始值
    private LocalDateTime createTime;
}

它就一个作用:和数据库对齐。

DTO (Data Transfer Object,数据传输对象)--- 前端传进来的数据

前端发什么字段,DTO 就接什么字段。多一个不要,少了就报错。

java 复制代码
public class UserRegisterDTO {
    @NotBlank(message = "用户名不能为空")
    private String username;

    @Length(min = 6, max = 20)
    private String password;

    @Email
    private String email;
    // 没有 id,没有 createTime------前端不应该传这些
}

这时就会人问:为什么不用 PO 直接接?你想想,注册接口要是允许前端传 id,人家传个 1,说不定就把管理员账号覆盖了。DTO 其实就是一层白名单,不声明的字段一律不收。

VO(View Object,视图对象) --- 返回给前端看的

VO 负责把数据"打扮"好再出门。敏感字段删掉,状态码转成文字,时间戳格式化成字符串。

java 复制代码
public class UserVO {
    private Long id;
    private String username;
    private String email;
    private String statusDesc;    // "正常" 而不是 0
    private String createTime;    // "2024-01-15 14:30:00" 而不是 LocalDateTime
    // 绝对没有 password
}

DTO 和 VO 最容易搞混,记一个方向就行:DTO 是进门(前端 → 后端),VO 是出门(后端 → 前端)。

BO(Bussiness Object,业务对象) --- 只在 Service 内部用,不是必需品

当你需要同时操作多张表、中间还要算来算去的时候,可以把这些数据塞到一个 BO 里。项目简单的话完全可以不用。

java 复制代码
// 比如算订单总价,涉及用户、订单明细、优惠券三张表
public class OrderCalculateBO {
    private UserPO user;
    private List<OrderItemPO> items;
    private CouponPO coupon;
    private BigDecimal totalPrice;
}

数据怎么走的,一张图就够了:

java 复制代码
前端请求
    ↓  DTO(只收该收的字段)
Controller
    ↓  转成 PO,复杂场景用 BO
Service
    ↓  操作完,转成 VO
Controller
    ↓  VO(只给该给的字段)
前端

一个完整的例子

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public Result<Void> register(@RequestBody @Valid UserRegisterDTO dto) {
        userService.register(dto);
        return Result.success();
    }

    @GetMapping("/{id}")
    public Result<UserVO> getUser(@PathVariable Long id) {
        return Result.success(userService.getUserById(id));
    }
}

转换逻辑全部放 Service,Controller 只负责收发:

java 复制代码
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public void register(UserRegisterDTO dto) {
        UserPO user = new UserPO();
        user.setUsername(dto.getUsername());
        user.setPassword(BCrypt.hashpw(dto.getPassword())); // 明文进来,密文存
        user.setEmail(dto.getEmail());
        user.setStatus(1);  // 新用户默认启用
        userMapper.insert(user);
    }

    @Override
    public UserVO getUserById(Long id) {
        UserPO user = userMapper.selectById(id);
        if (user == null) {
            throw new BusinessException("用户不存在");
        }
        UserVO vo = new UserVO();
        vo.setId(user.getId());
        vo.setUsername(user.getUsername());
        vo.setEmail(user.getEmail());
        vo.setStatusDesc(user.getStatus() == 1 ? "正常" : "禁用");
        vo.setCreateTime(user.getCreateTime()
            .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        return vo;  // password 没设,前端拿不到
    }
}

手动 set 太烦怎么办

字段少还好,字段一多就变体力活。两个方案:

BeanUtils.copyProperties ---

简单场景够用,但只能复制同名同类型的字段,status → statusDesc 这种情况还是得自己写。

java 复制代码
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo);
vo.setStatusDesc(user.getStatus() == 1 ? "正常" : "禁用");

MapStruct ---

稍微正规点的项目都用这个。写个接口,编译期自动生成实现,零反射,性能跟手写差不多。

java 复制代码
@Mapper(componentModel = "spring")
public interface UserConverter {
    UserVO toVO(UserPO po);
    UserPO toEntity(UserRegisterDTO dto);
}

容易踩的坑

把 PO 直接返回给前端 --- 密码、手机号、内部状态码全出去了。说出来你可能不信,这种事情在 Code Review 里见得太多了。

用同一个 DTO 接所有接口 --- 注册和编辑的参数差很多,硬塞到一个类里只会越来越臃肿,校验注解也越写越拧巴。一个场景一个 DTO,多写几个类不丢人哈。

VO 和 DTO 互用 --- 省一时的事,等后面改起来哭都来不及。哪天需求让加个字段,都不知道这字段是该进门还是该出门。

一表总结

|-----|--------------|------------|
| 对象 | 干什么的 | 方向 |
| PO | 跟数据库对齐 | 数据库<一>后端 |
| DTO | 收前端参数 | 前端<一>后端 |
| VO | 返回给前端看 | 后端<一>前端 |
| BO | Service内部算数据 | Service内部 |

如果这篇文章对您有帮助的话,还请留下一个免费的小心心

关注我,我们下期再见!

相关推荐
唐青枫1 小时前
Java Spring Data JPA 实战指南:Repository 查询、分页与实体映射
java
2601_961845421 小时前
2026四级作文预测26年|英语四级写作范文+模板PDF
java·数据库·spring·eclipse·pdf·tomcat·hibernate
DevOpenClub1 小时前
用 OCR、PDF 转文本和摘要接口构建 RAG 文档入库 Agent
数据库·pdf·ocr
不爱土豆唯爱马铃薯2 小时前
MC-028 | 团队协作
状态模式
wuminyu3 小时前
Java锁机制之park与futex系统级协同机制解析
java·linux·c语言·jvm·c++
疯狂打码的少年3 小时前
编译程序与解释程序的区别
java·开发语言·笔记
睡不醒男孩0308237 小时前
第二篇:深入探索开源数据库高可用:构建基于CLup的PostgreSQL生产级高可用与读写分离架构
数据库·postgresql·开源·clup
xieliyu.9 小时前
Java算法精讲:双指针(三)
java·开发语言·算法
明夜之约10 小时前
Spring Boot 自动装配源码
java·spring boot·后端