前言

很多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_flagcreate_userupdate_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架构 传统三层架构

核心原则:

  1. PO管数据库,DTO管接收,VO管展示,BO管业务,DO管领域

  2. 各司其职,互不干扰

  3. 不要为了分层而分层,恰到好处才是关键

检查清单(Code Review时用):

  • 接口入参用的是DTO而不是Entity?

  • 接口出参用的是VO而不是Entity?

  • Entity里的敏感字段(如deleteFlag)没有暴露给前端?

  • BO只在Service内部使用,没有传到Controller?

  • 没有创建多余的BO(简单场景直接转)?

  • 没有把DTO当VO用?

  • 项目中的命名规范统一?

希望这篇文章能帮你彻底搞懂这些概念,在实际项目中合理地使用它们,既不过度分层,也不分层混乱。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐