Spring Boot 参数校验工作流:DTO、注解和统一错误响应
来源:17golang原创
时间:2026-06-27 19:27:09 495浏览 收藏
Java 后端接口最容易混乱的地方之一,是参数校验散落在控制器、服务层和数据库异常里。短期看只是多写几个 if,长期会变成错误信息不一致、前端无法定位字段、测试用例难覆盖。
本文把 Spring Boot 参数校验整理成一个完整工作流:先明确接口边界,再用 DTO 承接输入,用注解声明规则,用 @Valid 触发校验,最后通过统一异常处理返回稳定的错误结构。
- 目标和边界:校验应该解决什么问题
- 全流程总览:从请求体到错误响应
- 阶段一:用 DTO 明确输入边界
- 阶段二:用注解声明字段规则
- 阶段三:统一错误响应结构
- 推荐流程:从新增接口到回归检查
- 常见误区和速查表
目标和边界:校验应该解决什么问题
参数校验的目标不是把所有业务规则都塞进注解,而是先拦住明显不合法的输入,让控制器收到的数据满足基本形状。例如用户名不能为空、手机号格式不对、页码不能小于 1、金额不能为负数。
建议把规则分成三类:
- 结构规则:字段是否必填、长度范围、数值范围、集合大小。
- 格式规则:邮箱、手机号、日期字符串、枚举值。
- 业务规则:库存是否足够、用户是否有权限、订单状态是否允许变更。
前两类适合放在 DTO 校验里,第三类通常放在服务层。边界清楚后,代码会更稳定。
全流程总览:从请求体到错误响应
一个推荐的参数校验链路可以拆成五步:请求进入控制器、绑定到 DTO、触发注解校验、捕获字段错误、返回统一响应。这样每个接口都沿用同一套入口和错误格式。

阶段一:用 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.validation 或 javax.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);
}
}
阶段三:统一错误响应结构
校验失败时,前端最需要知道两个信息:哪个字段错了,以及错误原因是什么。不要只返回一句“参数错误”,否则前端无法高亮具体输入框。

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": "手机号格式不正确"
}
]
}
推荐流程:从新增接口到回归检查
新增一个写接口时,可以按下面的顺序推进:
- 先定义请求 DTO,只放外部允许传入的字段。
- 给必填、长度、范围、格式规则添加注解。
- 控制器入口添加
@Valid和@RequestBody。 - 统一异常处理里返回字段级错误列表。
- 为必填为空、格式错误、边界值写接口测试。
- 把跨用户、权限、库存这类业务规则留在服务层。
常见误区和速查表
误区 1:DTO 和实体混用
实体字段通常比接口输入更多,混用会扩大可写范围。DTO 应该面向接口设计,实体面向存储设计。
误区 2:忘记添加 @Valid
字段上写了注解,但控制器入口没有 @Valid,校验不会按预期触发。排查时先看控制器方法签名。
误区 3:把复杂业务规则写进注解
库存、权限、订单状态这类规则依赖数据库或上下文,放在服务层更清晰。DTO 注解适合处理输入形状和基础格式。
速查表
| 目标 | 常用做法 | 检查点 |
|---|---|---|
| 字段必填 | @NotBlank、@NotNull |
空字符串和 null 都要测试 |
| 长度范围 | @Size |
测试最大长度边界 |
| 数值范围 | @Min、@Max |
测试最小值和最大值 |
| 格式规则 | @Pattern |
错误样例要覆盖常见输入 |
| 错误响应 | 统一异常处理 | 返回字段名和错误消息 |
总结一下,参数校验要形成工作流,而不是散落的条件判断。DTO 控制输入边界,注解声明基础规则,控制器触发校验,统一异常处理稳定错误响应。这样新接口越多,参数校验越容易复用和回归。
-
100 收藏
-
100 收藏
-
100 收藏
-
100 收藏
-
101 收藏
-
文章 · java教程 | 1小时前 | Java教程 · TTL缓存 · ConcurrentHashMap · 小项目 · java 本地缓存 concurrenthashmap TTL缓存 过期淘汰394 收藏
-
355 收藏
-
365 收藏
-
455 收藏
-
文章 · java教程 | 1星期前 | hashmap · 集合 · Java教程 · hashCode · equals · java HashMap map equals hashCode 可变key474 收藏
-
178 收藏
-
文章 · java教程 | 1星期前 | map · 并发安全 · 缓存设计 · Java教程 · java optional concurrenthashmap computeIfAbsent Map缓存236 收藏
-
204 收藏
-
文章 · java教程 | 1星期前 | Java · 集合 · ArrayList · Iterator · removeIf · java iterator ArrayList ConcurrentModificationException removeIf410 收藏
-
文章 · java教程 | 1星期前 | Java · 异步编程 · 后端开发 · CompletableFuture · 接口聚合 · java 结果合并 completablefuture 并行调用 超时兜底428 收藏
-
文章 · java教程 | 1星期前 | Java · 线程安全 · DateTimeFormatter · 日期处理 · 并发问题 · java 线程安全 日期格式化 threadlocal SimpleDateFormat DateTimeFormatter481 收藏
-
224 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习