Spring Boot 参数校验 Validation 终极指南

06-01 1229阅读

1. 概述

Spring Validation 基于 JSR-303(Bean Validation)规范,通过@Validated注解实现声明式校验。核心优势:

  • 零侵入性:基于 AOP 实现方法拦截校验
  • 规范统一:兼容 Bean Validation 标准注解
  • 功能扩展:支持分组校验、嵌套校验等高级特性
  • 高效开发:减少 80% 的参数校验代码量

    💡 关键区别:@Validated是 Spring 对@Valid的增强封装,支持分组校验,而@Valid支持嵌套校验

    2. 注解体系

    2.1 Bean Validation 标准注解

    分类注解说明
    空值检查@NotBlank字符串非空且 trim() 后长度 > 0(仅适用于字符串)
    @NotEmpty集合/数组元素数 > 0,字符串长度 > 0(适用于集合、数组、字符串)
    @NotNull字段值不能为 null
    @Null字段值必须为 null
    数值检查@DecimalMax(value)数值必须 ≤ 指定值(支持小数)
    @DecimalMin(value)数值必须 ≥ 指定值(支持小数)
    @Digits(integer,fraction)整数部分最多 integer 位,小数部分最多 fraction 位
    @Positive必须为正数
    @PositiveOrZero必须为正数或 0
    @Max(value)数值必须 ≤ 指定值(仅限整数)
    @Min(value)数值必须 ≥ 指定值(仅限整数)
    @Negative必须为负数
    @NegativeOrZero必须为负数或 0
    布尔检查@AssertTrue必须为 true
    @AssertFalse必须为 false
    长度检查@Size(min,max)字符串/集合/数组长度在 [min,max] 范围内
    日期检查@Future必须是将来日期
    @FutureOrPresent必须是将来或当前日期
    @Past必须是过去日期
    @PastOrPresent必须是过去或当前日期
    其他检查@Email符合邮箱格式(可配置宽松模式)
    @Pattern(regexp)符合正则表达式

    2.2 Hibernate Validator 扩展注解

    分类注解说明
    范围检查@Range(min,max)数值必须在 [min,max] 范围内(支持整型、BigDecimal)
    字符串检查@Length(min,max)字符串长度在 [min,max] 范围内
    格式检查@URL合法 URL 格式(可指定协议/主机/端口等参数)
    安全校验@SafeHtml过滤危险 HTML 标签(防御 XSS 攻击)
    其他检查@LuhnCheck银行卡号校验(Luhn 算法)
    @CNPJ巴西企业税号校验
    @CPF巴西个人税号校验

    2.3 @Valid vs @Validated

    特性@Valid@Validated
    分组校验❌ 不支持✅ 支持
    嵌套校验✅ 支持❌ 不支持
    校验触发自动触发需配合AOP使用

    3. 快速入门

    3.1 添加依赖

            
            
                org.springframework.boot
                spring-boot-starter-web
            
            
            
                org.springframework
                spring-aspects
            
            
            
                org.springframework.boot
                spring-boot-starter-test
                test
            
    
    
    • spring-boot-starter-web 依赖里,已经默认引入 hibernate-validator 依赖,所以本示例使用的是 Hibernate Validator 作为 Bean Validation 的实现框架。

      3.2 DTO 对象示例

      public class UserAddDTO {
          /**
           * 账号
           */
          @NotEmpty(message = "登录账号不能为空")
          @Length(min = 5, max = 16, message = "账号长度为 5-16 位")
          @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
          private String username;
          /**
           * 密码
           */
          @NotEmpty(message = "密码不能为空")
          @Length(min = 4, max = 16, message = "密码长度为 4-16 位")
          private String password;
          
          // ... 省略 setting/getting 方法
      }
      

      3.3 启用校验

      @RestController
      @RequestMapping("/users")
      @Validated
      public class UserController {
          private Logger logger = LoggerFactory.getLogger(getClass());
          @GetMapping("/get")
          public void get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) {
              logger.info("[get][id: {}]", id);
          }
          @PostMapping("/add")
          public void add(@Valid UserAddDTO addDTO) {
              logger.info("[add][addDTO: {}]", addDTO);
          }
      }
      

      4. 统一异常处理

      4.1 @Valid 的异常处理

      当使用 @Valid 注解进行参数校验时,校验失败会抛出 MethodArgumentNotValidException

      全局拦截示例:

      @RestControllerAdvice
      public class GlobalExceptionHandler {
          /***
           * 触发场景
           * 对象参数校验失败(如 @RequestBody + @Valid)
           * 常见使用组合
           * @Valid + DTO 对象
           * 校验注解适用对象
           * 对象属性级校验(@NotNull/@Size 等)
           */
          @ExceptionHandler(value = MethodArgumentNotValidException.class)
          public Result handleValidException(MethodArgumentNotValidException e) {
              BindingResult bindingResult = e.getBindingResult();
              List errors = bindingResult.getFieldErrors()
                      .stream()
                      .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
                      .collect(Collectors.toList());
              return Result.error(400, "参数校验失败", errors);
          }
      }
      

      4.2 @Validated 的异常处理

      当使用 @Validated 注解时,需要分场景处理:

      场景 1:Controller 层方法参数校验

      如果直接在 Controller 方法参数上使用 @Validated 校验简单类型(如 @RequestParam、@PathVariable),校验失败会抛出 ConstraintViolationException。

      全局拦截示例:

      @RestControllerAdvice
      public class GlobalExceptionHandler {
          /***
           * 触发场景
           * 方法参数直接校验
           * 常见使用组合
           * @Validated + 方法参数校验
           * 校验注解适用对象
           * 方法参数级校验(@RequestParam + @NotBlank 等)
           * 需要在类上标注 @Validated 才能触发 ConstraintViolationException
           */
      	@ExceptionHandler(ConstraintViolationException.class)
      	public Result handleConstraintViolation(ConstraintViolationException e) {
      	    List errors = e.getConstraintViolations()
      	            .stream()
      	            .map(v -> v.getPropertyPath() + ": " + v.getMessage())
      	            .collect(Collectors.toList());
      	    return Result.error(400, "参数校验失败", errors);
      	}
      }
      

      场景 2:校验对象参数

      如果校验对象参数(如 @RequestBody),行为与 @Valid 一致,抛出 MethodArgumentNotValidException(处理方式同 @Valid)。

      完整异常处理配置

      @RestControllerAdvice
      public class GlobalExceptionHandler {
          /***
           * 触发场景
           * 对象参数校验失败 如 (@RequestBody + @Valid/@RequestBody + @Validated)
           * 常见使用组合
           * (@Valid + DTO 对象/@Validated + DTO 对象)
           * 校验注解适用对象
           * 对象属性级校验(@NotNull/@Size 等)
           */
          @ExceptionHandler(MethodArgumentNotValidException.class)
          public Result handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
              BindingResult result = e.getBindingResult();
              List errors = result.getFieldErrors()
                      .stream()
                      .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
                      .collect(Collectors.toList());
              return Result.error(400, "对象参数校验失败", errors);
          }
          /***
           * 触发场景
           * 方法参数直接校验
           * 常见使用组合
           * @Validated + 方法参数校验
           * 校验注解适用方法参数
           * 方法参数级校验(@RequestParam + @NotBlank 等)
           * 需要在类上标注 @Validated 才能触发 ConstraintViolationException
           */
          @ExceptionHandler(ConstraintViolationException.class)
          public Result handleConstraintViolation(ConstraintViolationException e) {
              List errors = e.getConstraintViolations()
                      .stream()
                      .map(v -> v.getPropertyPath() + ": " + v.getMessage())
                      .collect(Collectors.toList());
              return Result.error(400, "简单参数校验失败", errors);
          }
      }
      

      4.3 关键总结

      注解使用场景抛出异常
      @Valid校验对象参数(如 @RequestBody)MethodArgumentNotValidException
      @Validated校验简单类型参数(如 @RequestParam)ConstraintViolationException
      @Validated校验对象参数(需配合 @Valid 使用)MethodArgumentNotValidException

      5. 自定义约束

      在大多数项目中,无论是 Bean Validation 定义的约束,还是 Hibernate Validator 附加的约束,都是无法满足我们复杂的业务场景。所以,我们需要自定义约束。

      开发自定义约束一共只要两步:

      • 1)编写自定义约束的注解;
      • 2)编写自定义的校验器 ConstraintValidator 。

        下面,就让我们一起来实现一个自定义约束,用于校验参数必须在枚举值的范围内。

        5.1 ArrayValuable

        public interface ArrayValuable {
            /**
             * @return 数组
             */
            T[] array();
        } 
        

        5.2 CommonStatusEnum

        @Getter
        @AllArgsConstructor
        public enum CommonStatusEnum implements ArrayValuable {
            ENABLE(0, "开启"),
            DISABLE(1, "关闭");
            public static final Integer[] ARRAYS = Arrays.stream(values()).map(CommonStatusEnum::getStatus).toArray(Integer[]::new);
            /**
             * 状态值
             */
            private final Integer status;
            /**
             * 状态名
             */
            private final String name;
            @Override
            public Integer[] array() {
                return ARRAYS;
            }
        }
        

        5.3 @InEnum

        @Target({
                ElementType.METHOD,
                ElementType.FIELD,
                ElementType.ANNOTATION_TYPE,
                ElementType.CONSTRUCTOR,
                ElementType.PARAMETER,
                ElementType.TYPE_USE
        })
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        @Constraint(
                validatedBy = {InEnumValidator.class}
        )
        public @interface InEnum {
            /**
             * @return 实现 ArrayValuable 接口的类
             */
            Class> value();
            String message() default "必须在指定范围 {value}";
            Class[] groups() default {};
            Class values;
            @Override
            public void initialize(InEnum annotation) {
                ArrayValuable[] values = annotation.value().getEnumConstants();
                if (values.length == 0) {
                    this.values = Collections.emptyList();
                } else {
                    this.values = Arrays.asList(values[0].array());
                }
            }
            @Override
            public boolean isValid(Object value, ConstraintValidatorContext context) {
                // 为空时,默认不校验,即认为通过
                if (value == null) {
                    return true;
                }
                // 校验通过
                if (values.contains(value)) {
                    return true;
                }
                // 校验不通过,自定义提示语句
                context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
                context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
                        .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
                return false;
            }
        }
        

        5.5 UserUpdateStatusDTO

        public class UserUpdateStatusDTO{
            /**
             * 用户编号
             */
            @NotNull(message = "用户编号不能为空")
            private Integer id;
            /**
             * 状态
             */
            @NotNull(message = "状态不能为空")
            @InEnum(value = CommonStatusEnum .class, message = "状态必须是 {value}")
            private Integer status;
            
            // ... 省略 set/get 方法
        }
        

        5.6 UserController

        @PostMapping("/update_status")
        public void updateStatus(@Valid UserUpdateStatusDTO updateStatusDTO) {
            logger.info("[updateStatus][updateStatusDTO: {}]", updateStatusDTO);
        }
        

        6. 分组校验

        6.1 UserUpdateStatusDTO

        public class UserUpdateStatusDTO {
            /**
             * 分组 01 ,要求状态必须为 true
             */
            public interface Group01 {}
            /**
             * 状态 02 ,要求状态必须为 false
             */
            public interface Group02 {}
            
            /**
             * 状态
             */
            @AssertTrue(message = "状态必须为 true", groups = Group01.class)
            @AssertFalse(message = "状态必须为 false", groups = Group02.class)
            private Boolean status;
            // ... 省略 set/get 方法
        }
        

        6.2 UserController

        @PostMapping("/update_status_true")
        public void updateStatusTrue(@Validated(UserUpdateStatusDTO.Group01.class) UserUpdateStatusDTO updateStatusDTO) {
            logger.info("[updateStatusTrue][updateStatusDTO: {}]", updateStatusDTO);
        }
        @PostMapping("/update_status_false")
        public void updateStatusFalse(@Validated(UserUpdateStatusDTO.Group02.class) UserUpdateStatusDTO updateStatusDTO) {
            logger.info("[updateStatusFalse][updateStatusDTO: {}]", updateStatusDTO);
        }
        

        7. 手动触发校验

        @Service 
        public class ManualValidateService {
        	@Autowired
        	private Validator validator;
        	
        	public void validate(UserAddDTO addDTO) {
        		Set result = validator.validate(addDTO);
        	// 打印校验结果 // 
        	    for (ConstraintViolation constraintViolation : result) {
        	        // 属性:消息
        	        System.out.println(constraintViolation.getPropertyPath() + ":" + constraintViolation.getMessage());
            	}
        	}
        }
        

        掌握这些核心要点,你的 Spring Boot 参数校验体系将兼具 健壮性 与 可维护性!

        Spring Boot 参数校验 Validation 终极指南
        (图片来源网络,侵删)
        Spring Boot 参数校验 Validation 终极指南
        (图片来源网络,侵删)
        Spring Boot 参数校验 Validation 终极指南
        (图片来源网络,侵删)
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码