一文带你快速了解@Validated注解的使用

293 阅读4分钟

SpringBoot中使用@Validated进行参数校验

  • 通常我们都需要对前端传来的数据进行合法校验后再进行操作,但如果大量的用if进行判断不仅使代码冗余,自己写起来也烦,springboot实现的@Validated相关注解可以帮我们更加简洁方便的实现数据合法性校验

引入依赖


<dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
	
<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
</dependency>

springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入validation和web,而springboot-2.3之前的版本只需要引入 web 依赖就可以了。

对DTO对象进行参数校验

  • 需要进行校验的对象类

    @Data
    class DemoDTO{
    	@NotNull(message = "名字不能为空")
    	private String name;
    	
    	@Max(value = 100,message = "年龄不能超过一百")
    	private Integer age;
    }
    
    • 注解中的message为校验不合法时抛出的信息
  • 进行校验的Controller类

    @RestController
    class DemoController{
    	@GetMapping("/test")
    	pubilc String demoTest(@Validated DemoDTO dto){
    		return "ok";
    	}
    }
    
    • 方法中的@Validated告诉spring需要进行参数校验,当校验合法就会进入方法之中,不合法则会抛出异常

对方法上单参数进行校验

  • 对方法上的单参数进行校验时必须要在Controller类上加@Validated,告诉spring对方法进行单参数校验,否则无法生效

    @RestController
    @Validated
    class DemoController{
       @GetMapping("/test")
       public String demoTest(@Max(value = 100,message = "传入数字不能大于100") Integer num)	{
           return "OK";
       }
    }
    

自定义注解

  • 当我们需要实现自己的校验逻辑时就需要自己创建自定义注解并自定义校验逻辑

  • 创建自定义注解

    @Documented
    @Constraint(validatedBy = StateValidator.class) // 指定该注解由哪个类进行校验
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ValidState {
    
        int min() default 0;
    
        int max() default 1;
        String message() default "状态值不合法";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    }
    
  • 创建自定义校验逻辑

    public class StateValidator implements ConstraintValidator<ValidState, Integer> {
    
        private int min;
        private int max;
    
        /**
         * 获取state中设定的最大最小值
         *
         * @param constraintAnnotation annotation instance for a given constraint declaration
         */
        @Override
        public void initialize(ValidState constraintAnnotation) {
            min = constraintAnnotation.min();
            max = constraintAnnotation.max();
        }
    
        /**
         * 判断值是否在区间内(并未判断null)
         * @param value object to validate
         * @param context context in which the constraint is evaluated
         *
         * @return
         */
        @Override
        public boolean isValid(Integer value, ConstraintValidatorContext context) {
            if (value != null) {
                return value >= min && value <= max;
            }
            return true;
        }
    }
    
  • 在参数上使用和其他原有注解一致

    @ValidState // 不对属性赋值即为默认值
    @NotNull(message = "...")
    private Integer state;
    

全局异常处理器捕获异常

  • 直接返回异常的信息可读性低,我们可以借助全局异常处理器返回简洁的报错信息给前端,提高可读性
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 参数校验失败
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e){
        BindingResult bindingResult = e.getBindingResult();
        StringBuilder errorMessage = new StringBuilder();
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            errorMessage.append(fieldError.getDefaultMessage()).append("!");
        }
        log.error(errorMessage.toString());
        return Result.fail(errorMessage.toString(),null);
    }
    /**
     * 校验异常
     */
    @ExceptionHandler(value = BindException.class)
    public Result validationExceptionHandler(BindException e) {
        BindingResult bindingResult = e.getBindingResult();
        StringBuilder errorMessage = new StringBuilder();
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            errorMessage.append(fieldError.getDefaultMessage()).append("!");
        }
        return Result.fail(errorMessage.toString(),null);
    }
    /**
     * 自定义注解抛出的异常
     */
    @ExceptionHandler(value = ConstraintViolationException.class)
    public Result ConstraintViolationExceptionHandler(ConstraintViolationException ex) {
        Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
        Iterator<ConstraintViolation<?>> iterator = constraintViolations.iterator();
        List<String> msgList = new ArrayList<>();
        while (iterator.hasNext()) {
            ConstraintViolation<?> cvl = iterator.next();
            msgList.add(cvl.getMessageTemplate());
        }
        return Result.fail(String.join(",",msgList),null);
    }
}
  • 加上@RestControllerAdvice spring会将该类注册为一个全局控制器,再加上@ExceptionHandler指定异常处理方法,将该类作为全局的异常处理器,@ExceptionHandler的参数为该方法对应处理的类
  • 最后返回通用结果给前端提示错误信息

分组校验

  • 当有一个DTO被多个业务用到时,对参数的校验要求可能是不一样的,这时候就需要我们进行分组校验,实现不同的业务进行不同的校验

  • 创建分组接口

    • Validated有自己默认的组 Default.class,我们在创建自己的分组接口时需要继承该类
    public interface ValidGroup extends Default {
        interface create extends ValidGroup{}
        interface delete extends ValidGroup{}
    } 
    
    
    • 这里创建一个ValidGroup接口继承了Default类,再在ValidGroup内对不同业务进行分类,继承Default类的原因往下看
  • 给需要进行校验的类属性加上注解

    @Data
    class DemoDTO{
        @NotNull(message = "id不能为空", groups = ValidGroup.delete.class)
        private Integer id;
        
    	@NotNull(message = "名字不能为空",groups = ValidGroup.create.class)
    	private String name;
    	
    	@Max(value = 100,message = "年龄不能超过一百")
    	private Integer age;
    }
    
  • Controller中开启校验

    @RestController
    class DemoController{
    	@GetMapping("/create")
    	pubilc String create(@Validated(value = ValidGroup.create.class) DemoDTO dto){
    		return "ok";
    	}
        @GetMapping("/delete")
    	pubilc String delete(@Validated(value = ValidGroup.delete.class) DemoDTO dto){
    		return "ok";
    	}
    }
    
    • 此时create方法的参数校验中id可以为空,而其他两个不行;delete方法中id不能为空而其他两个可以

    • 而ValidGroup接口继承Default的好处也在此体现出来,如果不继承Default类,两个方法想要对age属性进行校验就必须这样写

      @Validated(value = {ValidGroup.delete.class,Default.class}
                 
      @Validated(value = {ValidGroup.create.class,Default.class}
      

      而继承了就不需要加上Default.class,就是说默认情况下的注解分组为Default.class