👉这是一个或许对你有用的社群
🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:
《项目实战(视频)》:从书中学,往事中“练”
《互联网高频面试题》:面朝简历学习,春暖花开
《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题
《精进 Java 学习指南》:系统学习,互联网主流技术栈
《必读 Java 源码专栏》:知其然,知其所以然
👉这是一个或许对你有用的开源项目
国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构
RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRM、AI大模型、IoT物联网等功能:
多模块:https://gitee.com/zhijiantianya/ruoyi-vue-pro
微服务:https://gitee.com/zhijiantianya/yudao-cloud
视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK17/21+SpringBoot3、JDK8/11+Spring Boot2双版本
来源:
一张图看懂数据怎么流
POJO:所有对象的白板底座
PO:数据库长什么样,它就长什么样
DAO:数据库的接线员
BO:业务规则活在这里
DTO:跨边界的搬运工
VO:前端要什么就给什么
一次请求的完整对象流转
什么时候该分、什么时候不必
第一次打开公司项目,包结构里一堆类:OrderPO、OrderVO、OrderDTO、OrderBO……同一个订单,为什么要写四个类?
做了几个项目才搞明白:不是为了显得复杂,是数据在系统里每流过一层,关心的东西就不一样。数据库关心字段映射,业务层关心逻辑判断,前端关心显示格式。一个类全干,改一处就崩一片。
一张图看懂数据怎么流
数据从数据库到用户屏幕,经过三道转手:
持久层:PO 对应数据库表,DAO 负责 CRUD
业务层:BO 执行业务逻辑,DTO 在层与层之间搬运数据
表现层:VO 把数据格式化成前端想要的样子
每个对象的边界明确,改一层不影响另一层。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
POJO:所有对象的白板底座
Plain Old Java Object——普通的 Java 对象,不继承框架基类,不实现框架接口,没有任何注解依赖。
// 这就是一个 POJO:干净、独立、不依赖任何框架 public class Order { private Long id; private Long userId; private BigDecimal amount; private Integer status; private LocalDateTime createTime; // getter / setter 省略 }PO 是加了@Entity的 POJO,DTO 是加了Serializable的 POJO,VO 是专门用于展示的 POJO。POJO 是白纸,加上不同的用途约束就变成了其他对象。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/yudao-cloud
视频教程:https://doc.iocoder.cn/video/
PO:数据库长什么样,它就长什么样
Persistent Object。一张t_order表对应一个OrderPO,字段名、类型严格映射:
@Entity @Table(name = "t_order") publicclass OrderPO { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "user_id", nullable = false) private Long userId; @Column(name = "total_amount", precision = 10, scale = 2) private BigDecimal totalAmount; @Column(name = "status") private Integer status; // 0-待支付 1-已支付 2-已取消 3-已退款 @Column(name = "pay_time") private LocalDateTime payTime; @Column(name = "create_time", updatable = false) private LocalDateTime createTime; // getter / setter }MyBatis 项目里 PO 不需要 JPA 注解,就是个干净的 POJO,映射关系写在 XML 里。
最常见的错误:Controller 直接返回 PO。后果——密码、盐值、删除标记等敏感字段直接暴露给前端;数据库改个字段名,前端立刻崩。
DAO:数据库的接线员
Data Access Object——封装所有数据库操作,让业务层看不到 SQL。
// JPA 风格:接口 + 方法名自动推导 SQL @Repository public interface OrderDAO extends JpaRepository<OrderPO, Long> { List<OrderPO> findByUserIdAndStatus(Long userId, Integer status); @Query("SELECT o FROM OrderPO o WHERE o.totalAmount > :minAmount AND o.createTime > :since") List<OrderPO> findLargeOrders(@Param("minAmount") BigDecimal minAmount, @Param("since") LocalDateTime since); }// MyBatis 风格:接口 + XML 映射 public interface OrderMapper { OrderPO selectById(@Param("id") Long id); List<OrderPO> selectByUserId(@Param("userId") Long userId); int insert(OrderPO order); int updateStatus(@Param("id") Long id, @Param("status") Integer status, @Param("updateTime") LocalDateTime updateTime); }DAO 的价值:今天用 MySQL,明天换 PostgreSQL,业务代码一行不改。数据源切换只影响 DAO 实现层。
BO:业务规则活在这里
Business Object——承载业务逻辑的对象,活在 Service 层,可能由多个 PO 组合而成。
"能不能下单"这个判断不属于 PO(PO 只管存数据),也不属于 DAO(DAO 只管读写),它属于 BO:
public class OrderBO { private OrderPO order; private UserPO user; private List<OrderItemPO> items; private CouponPO coupon; /** * 判断订单是否可以发起退款 * 规则:已支付 + 未发货 + 下单不超过7天 */ public boolean canRefund() { if (order.getStatus() != 1) returnfalse; // 非已支付 if (order.getShipTime() != null) returnfalse; // 已发货 return Duration.between(order.getCreateTime(), LocalDateTime.now()).toDays() <= 7; } /** * 计算实付金额:商品总价 - 优惠券抵扣 */ public BigDecimal calculatePayAmount() { BigDecimal itemTotal = items.stream() .map(i -> i.getPrice().multiply(BigDecimal.valueOf(i.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); if (coupon != null && coupon.isValid()) { return itemTotal.subtract(coupon.getDiscount()).max(BigDecimal.ZERO); } return itemTotal; } }实际项目里 BO 有时候就是@Service类本身,有时候是 Service 内部的中间对象。不用纠结形式,核心是:业务判断放这一层,不散落到 Controller 和 DAO 里。
DTO:跨边界的搬运工
Data Transfer Object——在层之间、服务之间搬运数据,只搬不算。
微服务时代 DTO 用得极多:A 服务调 B 服务,不可能把整个 PO 扔过去(字段暴露 + 耦合)。DTO 只暴露必要字段:
// 服务间传输用的 DTO,只包含对方需要的字段 publicclass OrderDTO implements Serializable { privatestaticfinallong serialVersionUID = 1L; private Long orderId; private Long userId; private String userName; // 跨服务带过来的冗余字段 private BigDecimal payAmount; private String statusDesc; // "已支付",不是 Integer 1 private LocalDateTime createTime; // 没有密码、没有内部状态码、没有关联对象 // getter / setter }// 请求参数也常用 DTO 封装 public class CreateOrderDTO { @NotNull(message = "商品ID不能为空") private Long productId; @Min(value = 1, message = "数量至少为1") private Integer quantity; private Long couponId; // 可选 // getter / setter }DTO 通常实现Serializable(走网络得序列化),结构扁平,不含业务方法。
VO:前端要什么就给什么
Value Object——离用户最近的对象,所有字段按前端需要来组织:
public class OrderVO { private Long id; private String orderNo; // "2024061800001" private String statusText; // "待发货",不是 Integer 2 private String statusColor; // "#FF6B00",前端直接渲染 private String totalAmountDisplay; // "¥ 299.00" private String createTimeDisplay; // "2024年6月18日 14:30" private String payTimeDisplay; // "3分钟前"(相对时间) private List<OrderItemVO> items; // 嵌套的商品列表 VO private Boolean canCancel; // 按钮是否可点 private Boolean canRefund; // getter / setter }数据库存的是status = 2、total_amount = 299.00、create_time = 2024-06-18 14:30:22。前端不需要知道这些原始值——VO 直接给格式化好的字符串、计算好的状态。
VO 和 DTO 最容易混淆:DTO 强调"传输",是中间载体,任意层都能用;VO 强调"展示",是终点站,只给前端。
一次请求的完整对象流转
以"查询订单详情"为例:
前端请求 → Controller 接收参数 → Service 调 DAO.selectById() 拿回 OrderPO → PO 组装成 BO,执行业务判断(canRefund? canCancel?) → BO 转成 DTO 返回给 Controller → Controller 把 DTO 转成 VO(格式化时间、金额、状态文案) → 返回前端每一步转换的理由:
PO 不暴露前端 →安全
BO 不跨服务传输 →解耦
DTO 不带业务方法 →职责清晰
VO 不对应数据库结构 →前后端独立演进
什么时候该分、什么时候不必
项目规模 | 建议 |
|---|---|
个人/Demo 项目 | 一个 Model 走天下,别折腾 |
3-5 人小项目 | PO + VO 就够,中间层按需加 |
多人协作/微服务 | 严格分层,PO/BO/DTO/VO 全套 |
判断标准:如果你改了一个数据库字段,前端 API 也得跟着改——说明你需要分层了。如果服务 A 改了内部逻辑,服务 B 也得重新编译——说明你的 DTO 边界没划好。
分层的本质不是为了"看起来专业",是为了改一处不牵动全身。当项目只有你一个人维护的时候,这些分层只是负担。但只要超过三个人协作,这套规矩就是省心的选择。
欢迎加入我的知识星球,全面提升技术能力。
👉 加入方式,“长按”或“扫描”下方二维码噢:
星球的内容包括:项目实战、面试招聘、源码解析、学习路线。
文章有帮助的话,在看,转发吧。 谢谢支持哟 (*^__^*)