乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

数据校验

本章聚焦 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字符串必须是合法邮箱格式Stringregexp(自定义正则)
@Pattern字符串必须匹配指定正则Stringregexp(必填)、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 无法建立对应关系。

本章与全局的关系:本章讲解了请求参数的合法性校验。下一章"异常处理"将深入讲解如何统一捕获和处理校验失败抛出的异常,以及自定义校验注解的实现方式。

上一页
数据绑定原理