涉及知识点
mindmap
后端结构
实体类
DTO
VO
标准数据流层级
在标准的 Java 后端开发(如 Spring Boot 项目)中,从 Controller 接收请求到最终执行 SQL 语句,数据流通常遵循经典的三层架构。
完整的数据流层级与调用链路如下:
前端请求 ➡️ Controller层 ➡️ Service层 ➡️ Mapper层 ➡️ SQL/数据库
以下是每一层的具体职责与数据流转细节:
1. Controller 层(控制层 / 表现层)-接收请求参数,返回响应数据
- 职责:它是整个数据流的“入口”。负责接收前端(或 Postman 等工具)发送的 HTTP 请求,解析请求中的参数(如 URL 路径参数、请求体 JSON 等),并将这些参数传递给 Service 层。
- 数据流动作:Controller 接收到请求后,不处理具体的业务逻辑,而是直接调用 Service 层的方法。最后,它会将 Service 层返回的最终结果,包装成统一的格式(如包含状态码、提示信息、数据的 JSON 对象)响应给前端。
2. Service 层(业务逻辑层)——处理业务,调用Mapper
- 职责:由service和对应impl组成,前者是方法声明(Interface),后者是方法实现。一般调用mapper(涉及数据处理),避免调用其他模块mapper(绕开业务逻辑直接从数据库获取是不明智的,容易打破业务逻辑完整性,造成幽灵数据),调用其他service(注意防止循环调用)。它是数据流的“大脑”。负责处理具体的业务逻辑、参数校验、权限判断以及事务控制(确保一连串的数据库操作要么全部成功,要么全部失败)。
- 数据流动作:Service 层接收 Controller 传来的原始数据,进行业务规则的处理(例如判断用户名是否重复、计算金额等)。处理完毕后,它会调用 Mapper 层的方法,将需要持久化的数据传下去,或者获取数据库中的数据并进行二次加工,最后返回给 Controller。
3.Mapper 层(数据访问层 / 持久层)-执行数据库操作,负责数据存取
- 职责:由mapper和对应xml组成,前者是方法声明(Interface),后者是方法实现。它是数据流的“搬运工”。专门负责与数据库打交道,不包含任何业务逻辑。
- 数据流动作:这一层通常是一个接口(Interface),它定义了增、删、改、查等抽象方法。Service 层调用这些接口方法时,底层框架(如 MyBatis)会将其映射为具体的数据库操作。
4. SQL 执行(数据库交互)
- 职责:数据流的“终点”。
- 数据流动作:在 Mapper 层背后,通常对应着一个 XML 配置文件或注解,里面写有真实的 SQL 语句(如
SELECT * FROM user WHERE id = ?)。框架会将 Service 层传递下来的参数填入 SQL 语句中,发送给数据库执行。数据库执行完毕后,将结果集返回给 Mapper 层,再原路向上层返回。
DTO和VO的应用场景
在数据流转的过程中,通常还会伴随着实体类(Entity/domain) 的传递。Entity 主要负责和数据库打交道, 数据跨层传输采取DTO,前端展示采取VO,DTO(Data Transfer Object)、VO(View Object) 就是来辅助实体类传递的,完成数据怎么存、怎么传、怎么显,简单项目可以直接采取 实体类。
所处文件结构
com/company/project
├── Application.java # 项目启动类
├── config # 【配置层】存放各种配置类(如数据库、Redis、拦截器配置等)
├── controller # 【控制层】对外提供 RESTful 接口,接收请求并返回响应
├── service # 【业务逻辑层】核心业务处理
│ ├── impl # Service 接口的具体实现类
├── mapper # 【持久层 / DAO层】直接和数据库交互(MyBatis 接口)
├── model # 【数据模型层】存放各类数据对象(DTO/VO/Entity等)
│ ├── entity # 数据库实体类,与数据库表结构一一对应
│ ├── dto # 数据传输对象,用于接收前端请求参数
│ ├── vo # 视图展示对象,用于封装返回给前端的数据
│ ├── bo # 业务对象,用于 Service 层内部复杂业务数据的流转
│ ├── query # 查询参数对象,专门用于封装分页、筛选等复杂查询条件
│ └── enums # 全局枚举类(如订单状态、性别等)
├── exception # 【异常处理层】自定义业务异常类
├── handler # 全局异常处理器(统一拦截异常并返回友好提示)
├── constant # 【常量层】存放项目中的静态常量(如 Redis Key、业务常量)
├── utils # 【工具层】存放各种通用工具类(如日期、加密、字符串工具)
└── task / job # 【任务层】存放定时任务逻辑(如每日数据统计)
怎么用DTO和VO
在应用示例中,设计一个经典的电商场景: “用户订单详情页” 。在这个场景中,前端需要展示的数据不仅包含用户的基础信息,还包含该用户的历史订单列表以及订单总金额统计。
1. 基础实体类 (Entity)
首先,我们需要两个与数据库表一一对应的实体类:UserEntity(用户表)和 OrderEntity(订单表)。
// 用户实体类
@Data
@TableName("t_user")
public class UserEntity {
private Long id;
private String username;
private String password; // 敏感字段,绝对不能暴露给前端
private String email;
private LocalDateTime createTime;
private Integer status; // 数据库存的是数字状态码,如 1-正常, 2-冻结
}
// 订单实体类
@Data
@TableName("t_order")
public class OrderEntity {
private Long orderId;
private Long userId;
private BigDecimal amount; // 订单金额
private String orderNo;
private LocalDateTime createTime;
}
2. 数据传输对象 (DTO)
DTO 的核心作用是跨层传输。在 Service 层,我们需要把从不同表排查出来的UserEntity和List<OrderEntity>聚合在一起,打包传给 Controller 层。
// 用户订单聚合 DTO(Service层内部流转使用)
@Data
public class UserOrderDTO {
// 来自 UserEntity 的数据
private Long userId;
private String username;
private String email; // 在内部流转时,可以保留原始数据
private Integer userStatus; // 原始状态码
// 来自 OrderEntity 的聚合数据
private List<OrderEntity> orderList;// 直接携带订单实体列表进行内部传递
// 业务统计字段
private BigDecimal totalOrderAmount; // 订单总金额(业务层计算得出)
private Integer totalOrderCount; // 订单总数
}
3. 视图展示对象 (VO)
VO 的核心作用是适配前端展示。它必须对敏感数据进行脱敏,对格式进行美化,并且只保留前端页面真正需要的字段。
// 订单简要信息 VO(嵌套在用户VO中)
@Data
public class OrderBriefVO {
private String orderNo;
private String formattedAmount; // 格式化后的金额,如 "¥199.00"
private String orderDate; // 格式化后的日期,如 "2026-06-01"
}
// 用户订单详情 VO(最终返回给前端)
@Data
public class UserOrderVO {
private Long userId;
private String username; // 【数据脱敏】邮箱中间部分打码,如 "a****@gmail.com"
private String maskedEmail; // 【状态转换】将数字状态码转为前端直接展示的文本
private String userStatusText; // 如 "正常" 或 "已冻结"
// 【格式化数据】前端直接渲染,无需再次处理
private String formattedTotalAmount; // 如 "总计消费:¥5,999.00"
private String totalOrderCountDesc; // 如 "共 12 笔订单"
// 订单列表(使用专门定制的VO,而不是直接把OrderEntity扔给前端)
private List<OrderBriefVO> orders;
}
4. Service 层与 Controller 层调用示例
// ================= Service 层 =================
@Service
public class UserOrderService {
@Autowired private UserMapper userMapper;
@Autowired private OrderMapper orderMapper;
// 返回 DTO,负责把多个实体类的数据聚合起来
public UserOrderDTO getUserOrderDetail(Long userId) {
// 1. 分别查询用户和订单的 Entity
UserEntity user = userMapper.selectById(userId);
List<OrderEntity> orders = orderMapper.selectList(new QueryWrapper<OrderEntity>().eq("user_id", userId));
// 2. 组装 DTO
UserOrderDTO dto = new UserOrderDTO();
dto.setUserId(user.getId());
dto.setUsername(user.getUsername());
dto.setEmail(user.getEmail());
dto.setUserStatus(user.getStatus());
dto.setOrderList(orders);
// 3. 计算业务统计数据
dto.setTotalOrderCount(orders.size());
BigDecimal totalAmount = orders.stream() .map(OrderEntity::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add);
dto.setTotalOrderAmount(totalAmount);
return dto; // 将打包好的 DTO 交给 Controller
}
}
// ================= Controller 层 =================
@RestController
@RequestMapping("/api/users")
public class UserOrderController {
@Autowired private UserOrderService userOrderService;
@GetMapping("/{userId}/orders")
public Result<UserOrderVO> getUserOrders(@PathVariable Long userId) {
// 1. 调用 Service 获取聚合的 DTO
UserOrderDTO dto = userOrderService.getUserOrderDetail(userId);
// 2. 将 DTO 转换为适配前端的 VO
UserOrderVO vo = new UserOrderVO();
vo.setUserId(dto.getUserId());
vo.setUsername(dto.getUsername());
// 【脱敏处理】
vo.setMaskedEmail(maskEmail(dto.getEmail()));
// 【格式化与状态转换】
vo.setUserStatusText(dto.getUserStatus() == 1 ? "正常" : "已冻结");
vo.setFormattedTotalAmount("总计消费:¥" + dto.getTotalOrderAmount().toPlainString());
vo.setTotalOrderCountDesc("共 " + dto.getTotalOrderCount() + " 笔订单");
// 转换订单列表
List<OrderBriefVO> orderVOList = dto.getOrderList().stream().map(order -> {
OrderBriefVO briefVO = new OrderBriefVO(); briefVO.setOrderNo(order.getOrderNo());
briefVO.setFormattedAmount("¥" + order.getAmount().toPlainString());
briefVO.setOrderDate(order.getCreateTime().toLocalDate().toString());
return briefVO;
}).collect(Collectors.toList());
vo.setOrders(orderVOList);
// 3. 返回最终给前端的 VO
return Result.success(vo);
}
// 简单的邮箱脱敏工具方法
private String maskEmail(String email) {
if (email == null || !email.contains("@")) return email; String[] parts =
email.split("@");
return parts[0].charAt(0) + "****@" + parts[1];
}
}