数据校验
本章聚焦 Spring MVC 请求参数校验机制。在控制器方法接收表单或 JSON 参数时,数据合法性校验是保障系统健壮性的第一道防线。Spring MVC 与 JSR-303/JSR-380(Bean Validation)深度集成,通过
@Valid/@Validated触发校验,由BindingResult收集错误。本教程只讲解 Web 层的校验触发与错误处理,不涉及校验器底层实现原理。
定义与作用
Spring MVC 的数据校验建立在 Bean Validation 规范之上。开发者在实体类的字段上标注约束注解(如 @NotNull、@Size),然后在控制器方法的参数前加上 @Valid 或 @Validated,Spring MVC 就会在参数绑定完成后、Controller 方法执行前,自动调用校验器进行验证。
如果校验失败,错误信息会被封装到 BindingResult 对象中。开发者可以选择:
- 检查
BindingResult.hasErrors(),在方法内自行处理错误 - 不声明
BindingResult,让 Spring MVC 抛出MethodArgumentNotValidException,由全局异常处理器统一处理
生活类比:入职资料审核
想象飞翔科技 HR 审核新员工入职资料:
- 约束注解(
@NotNull、@Email):HR 手里的审核清单——"姓名不能为空""邮箱格式必须正确""手机号必须是 11 位" @Valid:HR 把清单贴在资料袋上,表示"这份资料需要按清单逐项审核"BindingResult:审核完成后,清单上打勾或打叉的记录。如果打叉了,HR 可以把资料退还给应聘者修改,而不是直接撕掉(抛异常)
核心原理
校验执行流程
当 Controller 方法的参数标注了 @Valid,Spring MVC 的 WebDataBinder 在绑定完请求数据后,会自动触发校验器(Validator)对目标对象进行校验:
关键约束:BindingResult 参数必须紧跟在被校验的参数之后声明。如果中间插入其他参数,Spring MVC 无法将错误对象与目标参数对应,会抛出异常。
适用位置与常用属性
校验触发注解
| 注解 | 适用位置 | 作用 |
|---|---|---|
@Valid | 方法参数、字段 | 标准 JSR-303 注解,触发校验,支持嵌套对象校验 |
@Validated | 类、方法参数 | Spring 扩展注解,支持分组校验(groups),不支持嵌套 |
常用约束注解
| 注解 | 作用 | 适用类型 | 常用属性 |
|---|---|---|---|
@NotNull | 值不能为 null | 任意 | — |
@NotEmpty | 值不能为 null 且不能为空(字符串长度 > 0,集合大小 > 0) | String、集合、数组、Map | — |
@NotBlank | 值不能为 null 且不能只包含空白字符 | String | — |
@Size | 长度/大小在指定范围内 | String、集合、数组 | min、max |
@Min | 数值必须 ≥ 指定值 | 数值类型 | value |
@Max | 数值必须 ≤ 指定值 | 数值类型 | value |
@Email | 字符串必须是合法邮箱格式 | String | regexp(自定义正则) |
@Pattern | 字符串必须匹配指定正则 | String | regexp(必填)、flags |
校验注解对比表
| 场景 | @NotNull | @NotEmpty | @NotBlank |
|---|---|---|---|
null | ❌ 不通过 | ❌ 不通过 | ❌ 不通过 |
""(空字符串) | ✅ 通过 | ❌ 不通过 | ❌ 不通过 |
" "(仅空格) | ✅ 通过 | ✅ 通过 | ❌ 不通过 |
"abc" | ✅ 通过 | ✅ 通过 | ✅ 通过 |
空集合 [] | ✅ 通过 | ❌ 不通过 | — |
选型建议:
- 字符串字段需要非空且有意义内容 → 用
@NotBlank - 集合/数组需要至少一个元素 → 用
@NotEmpty - 只需要引用不为 null(允许空字符串) → 用
@NotNull
完整示例
场景
飞翔科技员工管理系统中,后端小崔需要为"新增员工"接口添加参数校验。CTO 大翔要求:员工姓名不能为空、邮箱格式必须正确、年龄必须在 18~60 岁之间。架构师白歌建议使用 Bean Validation 实现,前端黄俪根据校验错误提示优化表单交互。
校验实体类
package com.feixiang.web.entity;
import javax.validation.constraints.*;
public class EmployeeForm {
@NotBlank(message = "员工姓名不能为空")
@Size(max = 20, message = "姓名长度不能超过 20 个字符")
private String name;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄不能小于 18 岁")
@Max(value = 60, message = "年龄不能大于 60 岁")
private Integer age;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
// getter / setter
}
控制器:使用 BindingResult 接收错误
package com.feixiang.web.controller;
import com.feixiang.web.entity.EmployeeForm;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/employees")
public class EmployeeController {
@PostMapping
public Object createEmployee(
@Valid @RequestBody EmployeeForm form,
BindingResult bindingResult) {
// 必须紧跟在 @Valid 参数之后
if (bindingResult.hasErrors()) {
Map<String, String> errors = new HashMap<>();
for (FieldError error : bindingResult.getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
return Map.of("success", false, "errors", errors);
}
// 校验通过,执行业务逻辑
return Map.of("success", true, "message", "员工 " + form.getName() + " 创建成功");
}
}
请求示例一:校验失败
curl -X POST http://localhost:8080/employees \
-H "Content-Type: application/json" \
-d '{
"name": "",
"age": 16,
"email": "invalid-email",
"phone": "123"
}'
响应:
{
"success": false,
"errors": {
"name": "员工姓名不能为空",
"age": "年龄不能小于 18 岁",
"email": "邮箱格式不正确",
"phone": "手机号格式不正确"
}
}
请求示例二:校验通过
curl -X POST http://localhost:8080/employees \
-H "Content-Type: application/json" \
-d '{
"name": "张三",
"age": 25,
"email": "zhangsan@feixiang.com",
"phone": "13800138000"
}'
响应:
{
"success": true,
"message": "员工 张三 创建成功"
}
分组校验示例(@Validated)
飞翔科技的"新增员工"和"更新员工"接口校验规则不同:更新时 ID 不能为空,新增时不需要。架构师白歌建议使用分组校验:
public interface CreateGroup {}
public interface UpdateGroup {}
public class EmployeeForm {
@NotNull(groups = UpdateGroup.class, message = "员工 ID 不能为空")
private Long id;
@NotBlank(groups = {CreateGroup.class, UpdateGroup.class})
private String name;
// ...
}
@PostMapping
public Object create(
@Validated(CreateGroup.class) @RequestBody EmployeeForm form,
BindingResult bindingResult) {
// 只触发 CreateGroup 组的校验规则
}
@PutMapping
public Object update(
@Validated(UpdateGroup.class) @RequestBody EmployeeForm form,
BindingResult bindingResult) {
// 只触发 UpdateGroup 组的校验规则
}
易错场景与面试考点
误区一:BindingResult 没有紧跟被校验参数
错误代码:
@PostMapping
public Object create(
@Valid @RequestBody EmployeeForm form,
@RequestHeader("X-Token") String token, // ❌ 错误:中间插入了其他参数
BindingResult bindingResult) {
// ...
}
错误现象:应用启动正常,但请求时抛出异常:
java.lang.IllegalStateException: Errors/BindingResult argument declared without preceding model attribute
纠正:BindingResult 必须紧跟在被 @Valid / @Validated 标注的参数之后,中间不能有任何其他参数。
误区二:@NotEmpty 和 @NotBlank 混用
错误代码:
@NotEmpty(message = "描述不能为空") // ❌ 对字符串应该用 @NotBlank
private String description;
问题:@NotEmpty 允许 " "(纯空格字符串)通过,而业务上通常希望拒绝纯空白内容。
纠正:字符串字段优先使用 @NotBlank,集合/数组字段使用 @NotEmpty,对象引用使用 @NotNull。
误区三:不声明 BindingResult,也不配置全局异常处理
错误代码:
@PostMapping
public Object create(@Valid @RequestBody EmployeeForm form) {
// 没有 BindingResult,也没有全局异常处理器
}
问题:校验失败时抛出 MethodArgumentNotValidException,如果没有 @ExceptionHandler 捕获,客户端会收到 500 错误页面,而不是结构化的 JSON 错误信息。
纠正方案:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(Map.of("errors", errors));
}
}
面试高频:@Valid 与 @Validated 的区别
| 维度 | @Valid | @Validated |
|---|---|---|
| 来源 | JSR-303 标准 | Spring 扩展 |
| 分组校验 | ❌ 不支持 | ✅ 支持 |
| 嵌套校验 | ✅ 支持(在嵌套对象字段上标注 @Valid) | ❌ 不支持 |
| 适用位置 | 参数、字段、方法 | 类、方法、参数 |
标准回答:日常开发中两者可以互换使用;需要分组校验时用 @Validated,需要对嵌套对象进行校验时用 @Valid。
小结
Spring MVC 通过 @Valid / @Validated 触发 Bean Validation,在请求参数绑定完成后自动执行字段校验。约束注解(@NotNull、@NotBlank、@Size、@Email 等)定义校验规则,BindingResult 收集校验错误。BindingResult 必须紧跟在被校验参数之后声明,否则 Spring MVC 无法建立对应关系。
本章与全局的关系:本章讲解了请求参数的合法性校验。下一章"异常处理"将深入讲解如何统一捕获和处理校验失败抛出的异常,以及自定义校验注解的实现方式。