下载工作台
Spring Boot Web 开发

DTO/VO 与 Bean Validation 进阶

试读上半部分 · 解锁后可读全文

第 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 出参                  │
       └─────────────────────────────────┘
层次命名习惯职责
EntityProduct, OrderJPA 映射表结构,含关联与懒加载
DTOProductCreateRequest创建/更新入参,无 id、无敏感字段
VOProductDetailVO出参视图,可含计算字段、脱敏

反例: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 / @DecimalMaxBigDecimal
@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>

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。