登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Spring Boot 参数校验工作流:DTO、注解和统一错误响应

来源:17golang原创

时间:2026-06-27 19:27:09 495浏览 收藏

Java 后端接口最容易混乱的地方之一,是参数校验散落在控制器、服务层和数据库异常里。短期看只是多写几个 if,长期会变成错误信息不一致、前端无法定位字段、测试用例难覆盖。

本文把 Spring Boot 参数校验整理成一个完整工作流:先明确接口边界,再用 DTO 承接输入,用注解声明规则,用 @Valid 触发校验,最后通过统一异常处理返回稳定的错误结构。

目录
  • 目标和边界:校验应该解决什么问题
  • 全流程总览:从请求体到错误响应
  • 阶段一:用 DTO 明确输入边界
  • 阶段二:用注解声明字段规则
  • 阶段三:统一错误响应结构
  • 推荐流程:从新增接口到回归检查
  • 常见误区和速查表

目标和边界:校验应该解决什么问题

参数校验的目标不是把所有业务规则都塞进注解,而是先拦住明显不合法的输入,让控制器收到的数据满足基本形状。例如用户名不能为空、手机号格式不对、页码不能小于 1、金额不能为负数。

建议把规则分成三类:

  • 结构规则:字段是否必填、长度范围、数值范围、集合大小。
  • 格式规则:邮箱、手机号、日期字符串、枚举值。
  • 业务规则:库存是否足够、用户是否有权限、订单状态是否允许变更。

前两类适合放在 DTO 校验里,第三类通常放在服务层。边界清楚后,代码会更稳定。

全流程总览:从请求体到错误响应

一个推荐的参数校验链路可以拆成五步:请求进入控制器、绑定到 DTO、触发注解校验、捕获字段错误、返回统一响应。这样每个接口都沿用同一套入口和错误格式。

Spring Boot 参数校验工作流,从请求体、DTO、注解校验、错误收集到统一响应
图 1:参数校验工作流把请求输入、字段规则和错误响应串成一条稳定链路。

阶段一:用 DTO 明确输入边界

不要直接把数据库实体当作请求参数对象。实体通常包含主键、创建时间、内部状态等字段,直接暴露给接口会让输入边界变模糊。推荐为每个写接口准备单独的请求 DTO。

public class CreateUserRequest {

    private String username;

    private String phone;

    private Integer age;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

DTO 的职责是描述外部输入。它不负责保存数据,也不负责判断复杂业务状态。

阶段二:用注解声明字段规则

Spring Boot 常用 Bean Validation 注解声明字段约束。不同版本项目可能使用 jakarta.validationjavax.validation 包,原则相同:规则写在 DTO 字段上,由框架在控制器入口触发。

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public class CreateUserRequest {

    @NotBlank(message = "用户名不能为空")
    @Size(max = 32, message = "用户名不能超过32个字符")
    private String username;

    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;

    @Min(value = 1, message = "年龄不能小于1")
    @Max(value = 120, message = "年龄不能大于120")
    private Integer age;

    // getter 和 setter 省略
}

控制器入口需要加上 @Valid,否则注解只是一组元数据,不会自动触发校验。

import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @PostMapping("/users")
    public ApiResult createUser(@Valid @RequestBody CreateUserRequest request) {
        Long userId = 1001L;
        return ApiResult.ok(userId);
    }
}

阶段三:统一错误响应结构

校验失败时,前端最需要知道两个信息:哪个字段错了,以及错误原因是什么。不要只返回一句“参数错误”,否则前端无法高亮具体输入框。

Spring Boot 参数校验错误响应路径,从字段错误到 errors 数组和统一响应体
图 2:统一错误响应应保留字段名、错误消息和稳定错误码。
import java.util.List;

public class ApiResult {

    private String code;
    private String message;
    private T data;

    public static  ApiResult ok(T data) {
        ApiResult result = new ApiResult();
        result.code = "OK";
        result.message = "success";
        result.data = data;
        return result;
    }

    public static ApiResult> badRequest(List errors) {
        ApiResult> result = new ApiResult();
        result.code = "BAD_REQUEST";
        result.message = "参数校验失败";
        result.data = errors;
        return result;
    }
}
public record FieldErrorItem(String field, String message) {
}

统一异常处理可以集中收集字段错误:

import java.util.List;

import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalErrorHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResult> handleInvalidBody(
            MethodArgumentNotValidException ex) {

        List errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> new FieldErrorItem(error.getField(), error.getDefaultMessage()))
                .toList();

        return ApiResult.badRequest(errors);
    }
}

返回结构可以保持稳定:

{
  "code": "BAD_REQUEST",
  "message": "参数校验失败",
  "data": [
    {
      "field": "phone",
      "message": "手机号格式不正确"
    }
  ]
}

推荐流程:从新增接口到回归检查

新增一个写接口时,可以按下面的顺序推进:

  1. 先定义请求 DTO,只放外部允许传入的字段。
  2. 给必填、长度、范围、格式规则添加注解。
  3. 控制器入口添加 @Valid@RequestBody
  4. 统一异常处理里返回字段级错误列表。
  5. 为必填为空、格式错误、边界值写接口测试。
  6. 把跨用户、权限、库存这类业务规则留在服务层。

常见误区和速查表

误区 1:DTO 和实体混用

实体字段通常比接口输入更多,混用会扩大可写范围。DTO 应该面向接口设计,实体面向存储设计。

误区 2:忘记添加 @Valid

字段上写了注解,但控制器入口没有 @Valid,校验不会按预期触发。排查时先看控制器方法签名。

误区 3:把复杂业务规则写进注解

库存、权限、订单状态这类规则依赖数据库或上下文,放在服务层更清晰。DTO 注解适合处理输入形状和基础格式。

速查表

目标 常用做法 检查点
字段必填 @NotBlank@NotNull 空字符串和 null 都要测试
长度范围 @Size 测试最大长度边界
数值范围 @Min@Max 测试最小值和最大值
格式规则 @Pattern 错误样例要覆盖常见输入
错误响应 统一异常处理 返回字段名和错误消息

总结一下,参数校验要形成工作流,而不是散落的条件判断。DTO 控制输入边界,注解声明基础规则,控制器触发校验,统一异常处理稳定错误响应。这样新接口越多,参数校验越容易复用和回归。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>