第 13 章 · DTO/VO、MapStruct 与 Bean Validation
本章目标:建立 DTO / VO / Entity 三层分离,禁止实体直出 API;使用 MapStruct 编译期生成映射代码;掌握 Bean Validation(@Valid、@NotNull 等)与 Spring @Validated 分组校验;在 shop-spring-demo 中重构商品与订单入参;为 ch14 SpringDoc 提供清晰 Schema,衔接 ch15 JWT 与安全进阶。
学时建议:4~5 小时(含 2 小时重构 shop-spring-demo)
前置:ch07 统一响应;ch12 MVP 骨架;了解 Java 注解基础。
13.1 为什么需要 DTO/VO
┌──────────────┐ DTO 入参 ┌─────────────┐ Entity ┌──────────┐
│ HTTP JSON │ ───────────────► │ Service │ ─────────────► │ MySQL │
└──────────────┘ └─────────────┘ └──────────┘
▲ │
│ VO 出参 │
└─────────────────────────────────┘
| 层次 | 命名习惯 | 职责 |
|---|---|---|
| Entity | Product, Order | JPA 映射表结构,含关联与懒加载 |
| DTO | ProductCreateRequest | 创建/更新入参,无 id、无敏感字段 |
| VO | ProductDetailVO | 出参视图,可含计算字段、脱敏 |
反例:Controller 直接返回 Product 实体 → 暴露 passwordHash、触发 LazyInitializationException、循环引用 JSON 爆炸。
正例:ProductDetailVO 仅含前端需要的字段,由 MapStruct 从 Entity 转换。
项目:shop-spring-demo;示例域名 api.example.com;禁止在 DTO 中携带真实内部系统 ID 规则说明。
13.2 包结构建议
com.example.shop.dto.product
ProductCreateRequest
ProductUpdateRequest
ProductQueryRequest
com.example.shop.vo.product
ProductListItemVO
ProductDetailVO
com.example.shop.mapper
ProductMapper (MapStruct)
与 ch12 的 dto/ 单包相比,按领域分子包更清晰。
13.3 Bean Validation 依赖
spring-boot-starter-validation 通常已由 Web Starter 传递引入。显式添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
常用注解:
| 注解 | 作用 |
|---|---|
@NotNull / @NotBlank | 非空 |
@Size(min,max) | 字符串/集合长度 |
@Min / @Max | 数值范围 |
@DecimalMin / @DecimalMax | BigDecimal |
@Email | 邮箱格式 |
@Pattern | 正则 |
@Positive | 正数 |
13.4 入参 DTO 示例
ProductCreateRequest.java:
package com.example.shop.dto.product;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
public class ProductCreateRequest {
@NotBlank(message = "商品名称不能为空")
@Size(max = 200, message = "名称最长 200 字")
private String name;
@NotNull(message = "价格不能为空")
@DecimalMin(value = "0.01", message = "价格必须大于 0")
@Digits(integer = 10, fraction = 2, message = "价格格式错误")
private BigDecimal price;
@NotNull
@Min(0)
private Integer stock;
@Size(max = 2000)
private String description;
// getters / setters
}
CreateOrderRequest.java:
public class CreateOrderRequest {
@NotEmpty(message = "订单行不能为空")
@Valid
private List<OrderLineRequest> items;
public static class OrderLineRequest {
@NotNull
private Long productId;
@NotNull
@Min(1)
@Max(999)
private Integer quantity;
// getters/setters
}
}
嵌套校验须在父字段加 @Valid。
13.5 Controller 启用 @Valid
@PostMapping("/api/products")
@PreAuthorize("hasRole('ADMIN')")
public Result<ProductDetailVO> create(@Valid @RequestBody ProductCreateRequest req) {
Product created = productService.create(req);
return Result.ok(productMapper.toDetailVO(created));
}
@PostMapping("/api/orders")
public Result<OrderDetailVO> createOrder(@Valid @RequestBody CreateOrderRequest req,
@AuthenticationPrincipal User user) {
Order order = orderService.createOrder(user.getId(), req);
return Result.ok(orderMapper.toDetailVO(order));
}
校验失败由全局异常处理器统一返回(ch07 扩展):
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.fail(40001, msg);
}
13.6 出参 VO 示例
ProductDetailVO.java:
package com.example.shop.vo.product;
import java.math.BigDecimal;
import java.time.Instant;
public class ProductDetailVO {
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
private Boolean published;
private String coverUrl;
private String description;
private Instant createdAt;
// getters/setters
}
ProductListItemVO 可省略 description 减轻列表载荷。
UserProfileVO 脱敏:
public class UserProfileVO {
private Long id;
private String username;
private String emailMasked; // a***@example.com
private String role;
}
13.7 引入 MapStruct
pom.xml:
<properties>
<mapstruct.version>1.5.5.Final</mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>