Spring MVC Bean 参数校验 @Validated

一、@Validated校验概述

1.1 核心说明

  • @ValidatedSpring 框架对 JSR303/JSR380(Java 原生参数校验规范)的增强注解 ,Spring 生态专属注解,功能全面优于JSR原生的@Valid
  • JSR303/JSR380 是Java官方定义的参数校验标准,提供基础校验注解,核心依赖包为jakarta.validation
  • 实际开发中依赖 Hibernate Validator,是JSR规范的最优实现,扩展了更多实用校验注解,是校验功能落地的核心
  • 核心价值:彻底替代硬编码的if/else参数判断逻辑,将参数校验规则与业务逻辑解耦,所有校验规则统一维护在实体类中,大幅精简业务代码
  • 核心区别:@Validated 完全兼容@Valid,核心增强了分组校验 能力,是Spring MVC/SpringBoot项目中Bean参数校验的首选方案

1.2 必备Maven依赖

注意:SpringBoot 2.3.x 及以上版本,spring-boot-starter-web 移除了校验相关依赖,必须手动引入;SpringBoot 2.2.x 及以下版本,仅引入spring-boot-starter-web即可,内置所有校验依赖。

xml 复制代码
<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.1.Final</version>
</dependency>
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.el</artifactId>
    <version>5.0.0</version>
</dependency>

二、@Validated 基础核心用法

核心执行流程

  1. 在需要校验的Java Bean实体类的属性上,添加校验注解配置校验规则+失败提示文案
  2. 在Controller接口的Bean入参前,添加@Validated注解标记,Spring MVC自动触发参数校验
  3. 校验通过 → 正常执行Controller中的业务逻辑
  4. 校验失败 → 触发校验异常,返回配置的失败提示信息

步骤1:定义实体类,配置校验注解

校验规则的核心配置处,所有校验注解配置在实体属性上,通过message属性自定义校验失败的提示信息,所有校验注解均来自jakarta.validation.constraints包。

java 复制代码
@Data
public class User {
    // 用户ID:不能为空 + 必须为正整数
    @NotNull(message = "用户ID不能为空")
    @Positive(message = "用户ID必须为正整数")
    private Long id;

    // 用户名:不能为空+非空字符串+长度2-10位,最常用的字符串非空校验
    @NotBlank(message = "用户名不能为空")
    @Length(min = 2, max = 10, message = "用户名长度需要在2~10个字符之间")
    private String username;

    // 密码:不能为空+长度6-20位
    @NotBlank(message = "登录密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度需要在6~20个字符之间")
    private String password;

    // 邮箱:非必填,填写则必须符合合法邮箱格式
    @Email(message = "邮箱格式不正确,请填写有效邮箱")
    private String email;

    // 手机号:非必填,填写则匹配手机号正则规则
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确,需填写11位有效手机号")
    private String phone;

    // 年龄:必填 + 取值范围18-60
    @NotNull(message = "年龄不能为空")
    @Min(value = 18, message = "年龄不能小于18岁")
    @Max(value = 60, message = "年龄不能大于60岁")
    private Integer age;
}

步骤2:Controller层使用@Validated触发校验(两种标准写法)

写法一:手动捕获校验结果 - BindingResult(入门推荐)

@Validated标记的入参后,紧跟BindingResult参数,Spring会自动将校验结果封装到该对象中,不会抛出全局异常,适合需要个性化返回校验结果的场景。

硬性规则:BindingResult 参数必须紧跟 在被@Validated注解标记的参数之后,一一对应,否则无法捕获校验结果。

java 复制代码
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Validated;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
public class UserController {

    @PostMapping("/user/save")
    public Map<String, Object> saveUser(@RequestBody @Validated User user, BindingResult bindingResult) {
        Map<String, Object> result = new HashMap<>(3);
        result.put("code", 200);
        result.put("msg", "操作成功");

        // 判断是否存在参数校验失败
        if (bindingResult.hasErrors()) {
            result.put("code", 400);
            result.put("msg", "参数校验失败");
            Map<String, String> errorMap = new HashMap<>();
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
            // 封装校验失败的字段和对应提示信息
            for (FieldError error : fieldErrors) {
                errorMap.put(error.getField(), error.getDefaultMessage());
            }
            result.put("data", errorMap);
            return result;
        }

        // 校验通过,执行业务逻辑
        result.put("data", user);
        return result;
    }
}

写法二:纯注解触发校验(生产最佳实践)

Controller层只需要在入参前添加@Validated注解,无需编写任何校验相关代码,校验失败的异常由全局统一异常处理器捕获处理,代码极致精简,无冗余,是项目开发的标准写法。

java 复制代码
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Validated;
import java.util.HashMap;
import java.util.Map;

@RestController
public class UserController {

    @PostMapping("/user/update")
    public Map<String, Object> updateUser(@RequestBody @Validated User user) {
        Map<String, Object> result = new HashMap<>(3);
        result.put("code", 200);
        result.put("msg", "用户信息修改成功");
        result.put("data", user);
        return result;
    }
}

三、全局统一异常处理器

核心说明

  • 使用@RestControllerAdvice注解定义全局异常处理器,作用于项目中所有的Controller层接口
  • 使用@ExceptionHandler注解指定要捕获的异常类型,Bean参数校验失败抛出的核心异常为MethodArgumentNotValidException
  • 统一封装接口返回格式,所有接口的异常返回结构完全一致,前端无需单独适配不同接口的异常格式
  • 可添加兜底异常捕获,处理项目中其他未捕获的系统异常
java 复制代码
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;

/**
 * 全局参数校验异常处理器
 * 统一捕获所有@Validated注解的Bean参数校验失败异常
 */
@RestControllerAdvice
public class GlobalValidExceptionHandler {

    /**
     * 捕获Bean参数校验失败的核心异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map<String, Object> handleValidException(MethodArgumentNotValidException e) {
        Map<String, Object> result = new HashMap<>(3);
        result.put("code", 400);
        result.put("msg", "参数校验失败");
        
        FieldError fieldError = e.getBindingResult().getFieldError();
        Map<String, String> errorInfo = new HashMap<>(1);
        if (fieldError != null) {
            errorInfo.put(fieldError.getField(), fieldError.getDefaultMessage());
        }
        result.put("data", errorInfo);
        return result;
    }

    /**
     * 兜底捕获项目中所有未处理的系统异常
     */
    @ExceptionHandler(Exception.class)
    public Map<String, Object> handleSystemException(Exception e) {
        Map<String, Object> result = new HashMap<>(3);
        result.put("code", 500);
        result.put("msg", "系统内部异常,请稍后重试");
        result.put("data", e.getMessage());
        return result;
    }
}

四、常用校验注解大全

所有注解均来自包 jakarta.validation.constraints,按使用频率排序,重点高频注解加粗标注,所有注解添加`标识

4.1 空值/空串校验(最高频)

  • @NotBlank :属性值不能为null + 不能为空字符串 + 不能全是空格,仅支持String类型,推荐用于用户名、密码、手机号等核心字符串字段
  • @NotNull :属性值不能为null,支持所有数据类型,对空字符串""不生效,适合所有非字符串类型的非空校验
  • @NotEmpty:属性值不能为null + 长度/元素个数大于0,支持String、数组、集合、Map类型

4.2 长度/范围校验

  • @Length(min=?,max=?) :指定字符串的长度区间,仅支持String类型
  • @Size(min=?,max=?):指定集合/数组的元素个数区间、字符串的长度区间,多类型兼容,通用性强
  • @Range(min=?,max=?):指定数值类型的取值区间,支持Integer、Long、Double等数值类型

4.3 数值合法性校验

  • @Min(value=?):数值必须大于等于指定值
  • @Max(value=?):数值必须小于等于指定值
  • @Positive:数值必须为正整数(> 0)
  • @PositiveOrZero:数值必须为正整数或0(≥ 0)
  • @Negative:数值必须为负整数(< 0)
  • @Digits(integer=?,fraction=?):限制数值的整数位和小数位长度,适合金额、百分比等场景

4.4 格式校验(常用)

  • @Email:校验字符串是否为合法的邮箱格式,可自定义正则补充校验规则
  • @Pattern(regexp=?):自定义正则表达式校验,万能注解,适合手机号、身份证号、邮编、验证码等个性化格式校验
  • @Past:日期类型必须是过去的时间,适合生日、创建时间等场景
  • @Future:日期类型必须是未来的时间,适合预约时间、过期时间等场景

五、@Validated 高级用法

5.1 分组校验

适用场景

同一个实体类,在新增、修改、查询等不同的业务场景下,需要的校验规则不同。例如:新增用户时ID无需校验(自增),修改用户时ID必须必填且合法;新增时密码必填,修改时密码非必填。

核心原理

给校验注解添加groups属性指定分组标识,在Controller层的@Validated中指定要生效的分组,即可按需触发不同的校验规则,实现一套实体类适配多套校验规则。

步骤1:定义分组标识(空接口,仅做标记)
java 复制代码
/**
 * 校验分组标记接口,仅做分组标识使用,无需实现任何方法
 */
// 新增业务场景分组
public interface AddGroup {}

// 修改业务场景分组
public interface UpdateGroup {}
步骤2:实体类中为校验注解指定分组
java 复制代码
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;

@Data
public class User {
    // 仅修改场景校验ID,新增场景不校验
    @NotNull(message = "用户ID不能为空", groups = UpdateGroup.class)
    @Positive(message = "用户ID必须为正整数", groups = UpdateGroup.class)
    private Long id;

    // 新增+修改场景都需要校验,指定多个分组
    @NotBlank(message = "用户名不能为空", groups = {AddGroup.class, UpdateGroup.class})
    private String username;

    // 仅新增场景校验密码,修改场景不校验
    @NotBlank(message = "密码不能为空", groups = AddGroup.class)
    private String password;
}
步骤3:Controller层指定分组触发校验
java 复制代码
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Validated;
import java.util.HashMap;
import java.util.Map;

@RestController
public class UserController {

    // 新增用户:仅触发AddGroup分组的校验规则
    @PostMapping("/user/add")
    public Map<String, Object> addUser(@RequestBody @Validated(AddGroup.class) User user) {
        Map<String, Object> result = new HashMap<>(3);
        result.put("code", 200);
        result.put("msg", "用户新增成功");
        result.put("data", user);
        return result;
    }

    // 修改用户:仅触发UpdateGroup分组的校验规则
    @PutMapping("/user/edit")
    public Map<String, Object> editUser(@RequestBody @Validated(UpdateGroup.class) User user) {
        Map<String, Object> result = new HashMap<>(3);
        result.put("code", 200);
        result.put("msg", "用户修改成功");
        result.put("data", user);
        return result;
    }
}

5.2 嵌套校验

适用场景

实体类中包含另一个实体类的对象属性,需要对嵌套的子实体属性也进行参数校验。例如:User实体包含Address收货地址实体,新增用户时需要同时校验用户信息和收货地址信息。

核心规则

嵌套的子实体属性上必须添加@Valid注解,外层的Controller入参添加@Validated注解,即可触发多层级的嵌套校验,缺一不可。

java 复制代码
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;

// 子实体:收货地址
@Data
public class Address {
    @NotBlank(message = "收货地址不能为空")
    private String addressDetail;
    @NotBlank(message = "收货人姓名不能为空")
    private String receiverName;
    @NotBlank(message = "收货人手机号不能为空")
    private String receiverPhone;
}

// 主实体:用户信息
@Data
public class User {
    @NotBlank(message = "用户名不能为空")
    private String username;
    @NotBlank(message = "密码不能为空")
    private String password;

    // 嵌套子实体,必须添加@Valid注解触发嵌套校验
    @Valid
    private Address address;
}

5.3 自定义校验注解(个性化业务校验)

适用场景

JSR标准注解无法满足业务的个性化校验需求,例如:手机号格式校验、状态值只能是0/1、身份证号校验、性别只能是男/女等场景,需要自定义校验规则。

步骤1:自定义校验注解
java 复制代码
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 指定校验规则的实现类
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface ValidPhone {
    // 校验失败的默认提示信息
    String message() default "手机号格式不正确,请填写11位有效手机号";
    // 分组校验必备属性,默认空即可
    Class<?>[] groups() default {};
    // 负载属性,默认空即可
    Class<? extends Payload>[] payload() default {};
}
步骤2:编写校验规则实现类
java 复制代码
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;

/**
 * 手机号校验规则实现类
 * 实现ConstraintValidator接口,泛型:自定义注解类,校验的属性类型
 */
public class PhoneNumberValidator implements ConstraintValidator<ValidPhone, String> {

    // 手机号正则表达式
    private static final String PHONE_REGEX = "^1[3-9]\\d{9}$";

    @Override
    public boolean isValid(String phone, ConstraintValidatorContext context) {
        // 手机号为空时不校验,如需必填可配合@NotBlank注解使用
        if (phone == null || phone.trim().isEmpty()) {
            return true;
        }
        // 匹配正则表达式
        return Pattern.matches(PHONE_REGEX, phone);
    }
}
步骤3:实体类中使用自定义校验注解
java 复制代码
import lombok.Data;

@Data
public class User {
    private String username;
    private String password;
    // 使用自定义的手机号校验注解
    @ValidPhone(message = "请填写正确的11位手机号")
    private String phone;
}

六、常见问题

6.1 校验注解不生效的常见原因

  1. SpringBoot 2.3.x+版本缺少校验依赖,未手动引入hibernate-validator核心包,是最常见的原因
  2. Controller层的入参前忘记添加@Validated注解,仅实体配置校验注解不会触发校验
  3. 校验注解使用错误:例如给非String类型使用@NotBlank、给空字符串使用@NotNull、注解配置在getter/setter方法上而非属性上
  4. 分组校验时,校验注解指定了groups分组,但@Validated中未指定对应的分组,导致注解不生效

6.2 @Validated@Valid 的核心区别

  1. @Validated是Spring框架的注解,支持分组校验@Valid是JSR原生注解,不支持分组校验
  2. 嵌套校验时,子实体的属性上必须使用@Valid注解,外层触发校验使用@Validated
  3. 纯基础校验场景下,两者的校验效果完全一致,可互换使用

七、核心总结

  1. @Validated 是Spring MVC中Bean参数校验的标准注解,核心价值是解耦校验规则与业务逻辑,替代硬编码的参数判断
  2. 基础用法:实体类配置校验注解 + Controller入参加@Validated + 全局异常处理器,是生产项目的标准写法,适配99%的业务场景
  3. 复杂场景:分组校验解决多业务场景复用实体、嵌套校验解决多层级实体校验、自定义注解解决个性化业务校验
  4. 后端参数校验是项目的基础规范,既能保证数据合法性,又能让业务代码更简洁、易维护、易扩展
相关推荐
无名-CODING4 小时前
Java Spring 事务管理深度指南
java·数据库·spring
蕨蕨学AI4 小时前
【Wolfram语言】45.2 真实数据集
java·数据库
予枫的编程笔记4 小时前
【Java集合】深入浅出 Java HashMap:从链表到红黑树的“进化”之路
java·开发语言·数据结构·人工智能·链表·哈希算法
ohoy4 小时前
RedisTemplate 使用之Set
java·开发语言·redis
mjhcsp4 小时前
C++ 后缀数组(SA):原理、实现与应用全解析
java·开发语言·c++·后缀数组sa
8***f3954 小时前
Spring容器初始化扩展点:ApplicationContextInitializer
java·后端·spring
程序猿零零漆4 小时前
Spring之旅 - 记录学习 Spring 框架的过程和经验(十四)SpringMVC的请求处理
学习·spring·pandas
r_oo_ki_e_4 小时前
java22--常用类
java·开发语言
linweidong5 小时前
C++ 中避免悬挂引用的企业策略有哪些?
java·jvm·c++