别再被VO、BO、PO、DTO、DO绕晕!今天用一篇文章把它们讲透
别再被VO、BO、PO、DTO、DO绕晕!今天用一篇文章把它们讲透
团队里新来的小伙伴在代码里各种对象传来传去,VO、BO、PO、DTO、DO满天飞,你问他为什么要这样设计,他支支吾吾说不清楚,最后你也被绕得云里雾里...
今天就来聊聊这些让人头疼的对象概念,用一篇文章把它们彻底讲透,让你再也不会被这些缩写绕晕!
一、为什么要区分这些对象?
在开始详细介绍之前,我们先来理解为什么要区分这些对象。很多新手开发者可能会问:"不就是个对象吗?为什么还要分这么细?直接用一个对象不就完事了?"
1.1 分层架构的需要
现代Web应用通常采用分层架构设计:
// 典型的分层架构
// Controller层 -> Service层 -> Repository层 -> Database
每一层都有不同的职责,需要处理不同的数据:
// Controller层需要的数据
public class UserVO {
private String username;
private String email;
private String avatar; // 头像URL
private String lastLoginTime; // 格式化的时间显示
}
// Service层需要的数据
public class UserBO {
private Long id;
private String username;
private String email;
private String password; // 密码需要在Service层处理
private Integer status; // 用户状态
private Date lastLoginTime;
}
// Repository层需要的数据
public class UserDO {
private Long id;
private String username;
private String email;
private String passwordHash; // 加密后的密码
private Integer status;
private Date lastLoginTime;
private Date createdAt;
private Date updatedAt;
}
1.2 数据安全和隐私保护
不同层面对数据的访问权限不同:
// 错误的做法 - 直接暴露数据库对象
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public UserDO getUser(@PathVariable Long id) {
// 直接返回数据库对象,可能包含敏感信息
return userService.getUserById(id);
}
}
// 正确的做法 - 使用专门的VO对象
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public UserVO getUser(@PathVariable Long id) {
UserBO userBO = userService.getUserById(id);
// 转换为VO对象,过滤敏感信息
return convertToVO(userBO);
}
private UserVO convertToVO(UserBO userBO) {
UserVO vo = new UserVO();
vo.setUsername(userBO.getUsername());
vo.setEmail(userBO.getEmail());
vo.setAvatar(buildAvatarUrl(userBO.getId()));
vo.setLastLoginTime(formatTime(userBO.getLastLoginTime()));
// 注意:不包含密码等敏感信息
return vo;
}
}
1.3 代码可维护性
合理的对象分层能提高代码的可维护性:
// 清晰的职责分离
// Controller层:处理HTTP请求,参数校验,返回响应
// Service层:业务逻辑处理
// Repository层:数据访问
// 各层使用不同的对象,职责明确
二、各种对象详解
2.1 PO (Persistent Object) - 持久化对象
PO是与数据库表结构一一对应的对象,通常与ORM框架(如MyBatis、Hibernate)配合使用。
// PO对象 - 与数据库表结构对应
@Entity
@Table(name = "users")
public class UserPO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username")
private String username;
@Column(name = "email")
private String email;
@Column(name = "password_hash")
private String passwordHash;
@Column(name = "status")
private Integer status;
@Column(name = "created_at")
private Date createdAt;
@Column(name = "updated_at")
private Date updatedAt;
@Column(name = "last_login_time")
private Date lastLoginTime;
// getters and setters
}
特点:
- 与数据库表结构一一对应
- 包含数据库字段的所有信息
- 通常由ORM框架管理生命周期
- 不包含业务逻辑
2.2 DO (Data Object) - 数据对象
DO与PO非常相似,也是与数据库表结构对应的对象,但在某些团队中会有一些细微差别。
// DO对象 - 数据访问层使用的对象
public class UserDO {
private Long id;
private String username;
private String email;
private String passwordHash;
private Integer status;
private Date createdAt;
private Date updatedAt;
private Date lastLoginTime;
// 可能包含一些数据访问层的辅助方法
public boolean isActive() {
return this.status == 1;
}
public boolean isLocked() {
return this.status == 2;
}
// getters and setters
}
特点:
- 与数据库表结构对应
- 在Repository层使用
- 可能包含一些简单的数据处理方法
- 不包含复杂业务逻辑
2.3 DTO (Data Transfer Object) - 数据传输对象
DTO用于在不同层之间传输数据,特别是在远程调用时使用。
// DTO对象 - 用于服务间数据传输
public class UserDTO {
private Long id;
private String username;
private String email;
private String statusDesc; // 状态描述,便于前端显示
private String createTime; // 格式化的时间字符串
// 用于创建用户的DTO
public static class CreateUserRequest {
private String username;
private String email;
private String password;
// getters and setters
}
// 用于更新用户的DTO
public static class UpdateUserRequest {
private String username;
private String email;
// getters and setters
}
// 用于用户列表的DTO
public static class UserListItem {
private Long id;
private String username;
private String email;
private String statusDesc;
// getters and setters
}
// getters and setters
}
特点:
- 用于层间数据传输
- 可以包含多个业务对象的数据
- 通常用于远程调用
- 可以进行数据格式转换
2.4 BO (Business Object) - 业务对象
BO包含业务逻辑的对象,通常在Service层使用。
// BO对象 - 包含业务逻辑的对象
public class UserBO {
private Long id;
private String username;
private String email;
private String password;
private Integer status;
private Date lastLoginTime;
private List<RoleBO> roles; // 用户角色
// 业务方法
public boolean canLogin() {
return this.status == 1; // 只有激活状态的用户才能登录
}
public boolean isAdmin() {
return this.roles.stream()
.anyMatch(role -> "ADMIN".equals(role.getRoleName()));
}
public void updateLastLoginTime() {
this.lastLoginTime = new Date();
}
public boolean checkPassword(String inputPassword) {
return BCrypt.checkpw(inputPassword, this.password);
}
// getters and setters
}
// 角色业务对象
public class RoleBO {
private Long id;
private String roleName;
private String roleDesc;
// getters and setters
}
特点:
- 包含业务逻辑
- 在Service层使用
- 可能聚合多个DO/PO对象
- 是业务的核心抽象
2.5 VO (Value Object) - 视图对象
VO是展示层使用的对象,通常用于向前端返回数据。
// VO对象 - 用于前端展示
public class UserVO {
private Long id;
private String username;
private String email;
private String avatar; // 头像URL
private String statusDesc; // 状态描述
private String lastLoginTime; // 格式化的时间显示
private List<String> roleNames; // 角色名称列表
// 嵌套的VO对象
public static class UserDetailVO {
private Long id;
private String username;
private String email;
private String avatar;
private String statusDesc;
private String lastLoginTime;
private List<RoleVO> roles;
private String registerTime;
// getters and setters
}
public static class RoleVO {
private String roleName;
private String roleDesc;
// getters and setters
}
// getters and setters
}
特点:
- 用于前端展示
- 数据格式友好
- 可能包含格式化后的数据
- 不包含敏感信息
三、对象转换实践
在实际开发中,我们需要在不同对象之间进行转换。
3.1 手动转换
// 手动转换示例
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public UserVO getUserById(Long id) {
// 1. 从数据库获取DO对象
UserDO userDO = userRepository.findById(id);
// 2. 转换为BO对象
UserBO userBO = convertToBO(userDO);
// 3. 转换为VO对象
UserVO userVO = convertToVO(userBO);
return userVO;
}
private UserBO convertToBO(UserDO userDO) {
if (userDO == null) {
return null;
}
UserBO userBO = new UserBO();
userBO.setId(userDO.getId());
userBO.setUsername(userDO.getUsername());
userBO.setEmail(userDO.getEmail());
userBO.setPassword(userDO.getPasswordHash());
userBO.setStatus(userDO.getStatus());
userBO.setLastLoginTime(userDO.getLastLoginTime());
return userBO;
}
private UserVO convertToVO(UserBO userBO) {
if (userBO == null) {
return null;
}
UserVO userVO = new UserVO();
userVO.setId(userBO.getId());
userVO.setUsername(userBO.getUsername());
userVO.setEmail(userBO.getEmail());
userVO.setAvatar(buildAvatarUrl(userBO.getId()));
userVO.setStatusDesc(getStatusDesc(userBO.getStatus()));
userVO.setLastLoginTime(formatTime(userBO.getLastLoginTime()));
return userVO;
}
private String buildAvatarUrl(Long userId) {
return "https://cdn.example.com/avatar/" + userId + ".jpg";
}
private String getStatusDesc(Integer status) {
switch (status) {
case 1: return "激活";
case 2: return "锁定";
case 3: return "注销";
default: return "未知";
}
}
private String formatTime(Date date) {
if (date == null) {
return "";
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
}
3.2 使用MapStruct自动转换
// 使用MapStruct简化对象转换
// 1. 添加依赖
/*
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.2.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.2.Final</version>
<scope>provided</scope>
</dependency>
*/
// 2. 定义转换接口
@Mapper
public interface UserConverter {
UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);
// DO转BO
UserBO toBO(UserDO userDO);
// BO转VO
UserVO toVO(UserBO userBO);
// 自定义转换规则
@Mapping(target = "statusDesc", expression = "java(getStatusDesc(userBO.getStatus()))")
@Mapping(target = "lastLoginTime", expression = "java(formatTime(userBO.getLastLoginTime()))")
UserVO toVOWithCustom(UserBO userBO);
default String getStatusDesc(Integer status) {
switch (status) {
case 1: return "激活";
case 2: return "锁定";
case 3: return "注销";
default: return "未知";
}
}
default String formatTime(Date date) {
if (date == null) {
return "";
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
}
// 3. 使用转换器
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public UserVO getUserById(Long id) {
// 1. 从数据库获取DO对象
UserDO userDO = userRepository.findById(id);
// 2. 转换为BO对象
UserBO userBO = UserConverter.INSTANCE.toBO(userDO);
// 3. 转换为VO对象
UserVO userVO = UserConverter.INSTANCE.toVOWithCustom(userBO);
return userVO;
}
}
四、实际应用场景
4.1 用户管理系统的完整示例
// 完整的用户管理系统示例
// 1. 数据库实体 (PO/DO)
@Entity
@Table(name = "users")
public class UserPO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username")
private String username;
@Column(name = "email")
private String email;
@Column(name = "password_hash")
private String passwordHash;
@Column(name = "status")
private Integer status;
@Column(name = "created_at")
private Date createdAt;
@Column(name = "updated_at")
private Date updatedAt;
// getters and setters
}
// 2. 数据访问层
@Repository
public class UserRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public UserDO findById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{id}, new UserRowMapper());
}
public List<UserDO> findAll() {
String sql = "SELECT * FROM users";
return jdbcTemplate.query(sql, new UserRowMapper());
}
public void save(UserDO userDO) {
if (userDO.getId() == null) {
// 插入新用户
String sql = "INSERT INTO users (username, email, password_hash, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)";
jdbcTemplate.update(sql, userDO.getUsername(), userDO.getEmail(), userDO.getPasswordHash(),
userDO.getStatus(), new Date(), new Date());
} else {
// 更新用户
String sql = "UPDATE users SET username = ?, email = ?, password_hash = ?, status = ?, updated_at = ? WHERE id = ?";
jdbcTemplate.update(sql, userDO.getUsername(), userDO.getEmail(), userDO.getPasswordHash(),
userDO.getStatus(), new Date(), userDO.getId());
}
}
}
// 3. 业务逻辑层
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public UserVO createUser(CreateUserDTO createUserDTO) {
// 1. DTO转BO
UserBO userBO = new UserBO();
userBO.setUsername(createUserDTO.getUsername());
userBO.setEmail(createUserDTO.getEmail());
userBO.setPassword(passwordEncoder.encode(createUserDTO.getPassword()));
userBO.setStatus(1); // 默认激活状态
// 2. 业务逻辑处理
if (userRepository.existsByUsername(userBO.getUsername())) {
throw new BusinessException("用户名已存在");
}
if (userRepository.existsByEmail(userBO.getEmail())) {
throw new BusinessException("邮箱已存在");
}
// 3. BO转DO并保存
UserDO userDO = convertToDO(userBO);
userRepository.save(userDO);
// 4. DO转BO再转VO
userBO.setId(userDO.getId());
UserVO userVO = convertToVO(userBO);
return userVO;
}
public UserVO getUserById(Long id) {
UserDO userDO = userRepository.findById(id);
if (userDO == null) {
throw new BusinessException("用户不存在");
}
UserBO userBO = convertToBO(userDO);
return convertToVO(userBO);
}
public List<UserListItemVO> getAllUsers() {
List<UserDO> userDOs = userRepository.findAll();
return userDOs.stream()
.map(this::convertToBO)
.map(this::convertToListItemVO)
.collect(Collectors.toList());
}
private UserDO convertToDO(UserBO userBO) {
UserDO userDO = new UserDO();
userDO.setId(userBO.getId());
userDO.setUsername(userBO.getUsername());
userDO.setEmail(userBO.getEmail());
userDO.setPasswordHash(userBO.getPassword());
userDO.setStatus(userBO.getStatus());
userDO.setCreatedAt(new Date());
userDO.setUpdatedAt(new Date());
return userDO;
}
private UserBO convertToBO(UserDO userDO) {
UserBO userBO = new UserBO();
userBO.setId(userDO.getId());
userBO.setUsername(userDO.getUsername());
userBO.setEmail(userDO.getEmail());
userBO.setPassword(userDO.getPasswordHash());
userBO.setStatus(userDO.getStatus());
userBO.setLastLoginTime(userDO.getUpdatedAt());
return userBO;
}
private UserVO convertToVO(UserBO userBO) {
UserVO userVO = new UserVO();
userVO.setId(userBO.getId());
userVO.setUsername(userBO.getUsername());
userVO.setEmail(userBO.getEmail());
userVO.setAvatar("https://cdn.example.com/avatar/" + userBO.getId() + ".jpg");
userVO.setStatusDesc(getStatusDesc(userBO.getStatus()));
userVO.setLastLoginTime(formatTime(userBO.getLastLoginTime()));
return userVO;
}
private UserListItemVO convertToListItemVO(UserBO userBO) {
UserListItemVO listItemVO = new UserListItemVO();
listItemVO.setId(userBO.getId());
listItemVO.setUsername(userBO.getUsername());
listItemVO.setEmail(userBO.getEmail());
listItemVO.setStatusDesc(getStatusDesc(userBO.getStatus()));
return listItemVO;
}
private String getStatusDesc(Integer status) {
switch (status) {
case 1: return "激活";
case 2: return "锁定";
case 3: return "注销";
default: return "未知";
}
}
private String formatTime(Date date) {
if (date == null) {
return "";
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
}
// 4. 控制器层
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<UserVO> createUser(@RequestBody @Valid CreateUserDTO createUserDTO) {
UserVO userVO = userService.createUser(createUserDTO);
return ResponseEntity.ok(userVO);
}
@GetMapping("/{id}")
public ResponseEntity<UserVO> getUserById(@PathVariable Long id) {
UserVO userVO = userService.getUserById(id);
return ResponseEntity.ok(userVO);
}
@GetMapping
public ResponseEntity<List<UserListItemVO>> getAllUsers() {
List<UserListItemVO> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
}
4.2 DTO对象定义
// DTO对象定义
// 创建用户请求DTO
public class CreateUserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20之间")
private String password;
// getters and setters
}
// 用户列表项VO
public class UserListItemVO {
private Long id;
private String username;
private String email;
private String statusDesc;
// getters and setters
}
// 用户详情VO
public class UserVO {
private Long id;
private String username;
private String email;
private String avatar;
private String statusDesc;
private String lastLoginTime;
// getters and setters
}
五、最佳实践建议
5.1 命名规范
// 推荐的命名规范
// PO: UserPO, OrderPO
// DO: UserDO, OrderDO
// DTO: UserDTO, CreateUserRequest, UpdateUserRequest
// BO: UserBO, OrderBO
// VO: UserVO, UserDetailVO, UserListItemVO
5.2 转换工具选择
// 1. 简单场景:手动转换
// 2. 复杂场景:MapStruct
// 3. 特殊场景:自定义转换器
// MapStruct配置示例
@Mapper(componentModel = "spring")
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
UserBO toBO(UserDO userDO);
UserVO toVO(UserBO userBO);
@Mapping(target = "password", ignore = true) // 忽略密码字段
UserVO toSafeVO(UserBO userBO);
}
5.3 分层使用建议
// 各层对象使用建议
// Controller层:主要使用VO和DTO
// Service层:主要使用BO
// Repository层:主要使用DO/PO
// 外部接口:主要使用DTO
5.4 性能考虑
// 避免不必要的转换
// 1. 只转换需要的字段
// 2. 批量操作时考虑批量转换
// 3. 缓存转换结果(如果适用)
// 批量转换示例
public List<UserVO> batchConvertToVO(List<UserBO> userBOs) {
return userBOs.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
}
六、常见误区
6.1 误区一:所有层都用同一个对象
// 错误的做法
@RestController
public class UserController {
@GetMapping("/users/{id}")
public UserDO getUser(@PathVariable Long id) {
// 直接返回数据库对象,可能暴露敏感信息
return userService.getUserById(id);
}
}
// 正确的做法
@RestController
public class UserController {
@GetMapping("/users/{id}")
public UserVO getUser(@PathVariable Long id) {
// 返回专门的VO对象,过滤敏感信息
return userService.getUserVOById(id);
}
}
6.2 误区二:对象之间随意转换
// 错误的做法
public class UserService {
public UserVO processUser(UserVO userVO) {
// 在Service层直接操作VO对象
// 违反了分层原则
return userVO;
}
}
// 正确的做法
public class UserService {
public UserVO processUser(UserDTO userDTO) {
// 1. DTO转BO
UserBO userBO = convertToBO(userDTO);
// 2. 在BO上执行业务逻辑
processBusinessLogic(userBO);
// 3. BO转VO
return convertToVO(userBO);
}
}
6.3 误区三:过度设计
// 错误的做法 - 过度分层
public class UserService {
public SimpleUserVO getSimpleUser(Long id) {
UserDO userDO = userRepository.findById(id);
UserBO userBO = userConverter.toBO(userDO);
UserDTO userDTO = userConverter.toDTO(userBO);
UserVO userVO = userConverter.toVO(userDTO);
SimpleUserVO simpleUserVO = userConverter.toSimpleVO(userVO);
return simpleUserVO;
}
}
// 正确的做法 - 合理分层
public class UserService {
public UserVO getUser(Long id) {
UserDO userDO = userRepository.findById(id);
UserBO userBO = userConverter.toBO(userDO);
return userConverter.toVO(userBO);
}
}
七、总结
通过今天的详细讲解,我们了解了各种对象的概念和使用场景:
- PO (Persistent Object):与数据库表结构对应的持久化对象
- DO (Data Object):数据访问层使用的数据对象
- DTO (Data Transfer Object):用于层间数据传输的对象
- BO (Business Object):包含业务逻辑的业务对象
- VO (Value Object):用于前端展示的视图对象
核心要点:
- 不同对象有不同的职责和使用场景
- 合理分层能提高代码的可维护性和安全性
- 对象转换是必要的,但要避免过度设计
- 选择合适的转换工具能提高开发效率
掌握了这些概念,相信你在项目开发中再也不会被这些缩写绕晕了!
今日思考:你们团队在项目中是如何使用这些对象的?有没有什么好的实践经验?欢迎在评论区分享你的看法!
如果你觉得这篇文章对你有帮助,欢迎分享给更多的朋友。关注"服务端技术精选",获取更多技术干货!
标题:别再被VO、BO、PO、DTO、DO绕晕!今天用一篇文章把它们讲透
作者:jiangyi
地址:http://jiangyi.space/articles/2025/12/21/1766304290667.html
- 一、为什么要区分这些对象?
- 1.1 分层架构的需要
- 1.2 数据安全和隐私保护
- 1.3 代码可维护性
- 二、各种对象详解
- 2.1 PO (Persistent Object) - 持久化对象
- 2.2 DO (Data Object) - 数据对象
- 2.3 DTO (Data Transfer Object) - 数据传输对象
- 2.4 BO (Business Object) - 业务对象
- 2.5 VO (Value Object) - 视图对象
- 三、对象转换实践
- 3.1 手动转换
- 3.2 使用MapStruct自动转换
- 四、实际应用场景
- 4.1 用户管理系统的完整示例
- 4.2 DTO对象定义
- 五、最佳实践建议
- 5.1 命名规范
- 5.2 转换工具选择
- 5.3 分层使用建议
- 5.4 性能考虑
- 六、常见误区
- 6.1 误区一:所有层都用同一个对象
- 6.2 误区二:对象之间随意转换
- 6.3 误区三:过度设计
- 七、总结
0 评论