《JSR303参数校验》

发布时间:2024年01月03日

一、基础概述

1.简介

Java API 规范 (JSR303) 定义了 Bean 校验的标准 validation-api,但没有提供实现。hibernate validation 是对这个规范的实现,并增加了校验注解如 @Email、@Length 等。Spring Validation 是对 hibernate validation 的二次封装,用于支持 spring mvc 参数自动校验。

2.依赖

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

如果spring-boot 版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator依赖。如果 spring-boot 版本大于 2.3.x,则需要手动引入依赖
在这里插入图片描述

二、参数效验

对于 web 服务来说,为防止非法参数对业务造成影响,在 Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:

POSTPUT 请求,使用@RequestBody传递参数
GET 请求,使用 @RequestParam / @PathVariable 传递参数

1.@RequestParam参数校验

@RestController
// 代表需要参数验证,一定要加
@Validated
public class HelloController {

    // @Min代表参数不能小于10
    @RequestMapping(value = "/test1")
    public Object m1(@RequestParam(value = "number") @Min(value = 10) Integer number) {
        System.out.println(number);
        return UUID.randomUUID() + "----" + number;
    }

}

【测试】

http://localhost:8081/test1?number=9 参数为9即报错,异常为ConstraintViolationException

http://localhost:8081/test1?number=12 参数为12即正常

2.@PathVariable参数效验

@RestController
@Validated
public class HelloController {

    @RequestMapping(value = "/test2/{number}")
    public Object m2(@PathVariable(value = "number") @Max(value = 20) String number) {
        System.out.println(number);
        return UUID.randomUUID() + "----" + number;
    }

}

3.@RequestBody参数效验

在实体类上生命效验字段

package com.h3c.entity;

import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;

@Data
public class UserParam {

    @NotNull
    private String userName;

    @Length(min = 6, max = 20, message = "长度范围为6~20")
    private String account;

    @Length(min = 6, max = 20)
    private String password;

}

在方法参数上声明校验注解@Validated

@RestController
public class HelloController {

    // 需要设置声明,@Valid和@Validated都可以
    @PostMapping(value = "/test3")
    public Object m3(@RequestBody @Validated UserParam param) {
        System.out.println(param);
        return UUID.randomUUID() + "----" + param;
    }
}

参数错误会报异常MethodArgumentNotValidException

4.全局异常处理

前面说过,如果校验失败,会抛出MethodArgumentNotValidException 或者ConstraintViolationException异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。

package com.h3c.exception;

import com.h3c.entity.Result;
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 javax.validation.ConstraintViolationException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@RestControllerAdvice
public class SystemExceptionHandler {
	
    // MethodArgumentNotValidException异常可以获取到异常字段
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        ex.printStackTrace();
        // 获取所有的错误字段
        List<FieldError> errors = ex.getBindingResult().getFieldErrors();
        Map<String, String> map = new LinkedHashMap<>();
        // 把错误信息存入一个map,然后返回
        errors.forEach(item -> {
            map.put(item.getField(), item.getDefaultMessage());
        });
        return Result.error(map);
    }

    @ExceptionHandler({ConstraintViolationException.class})
    public Result handleConstraintViolationException(ConstraintViolationException ex) {
        ex.printStackTrace();
        return Result.error(ex.getMessage());
    }
}

【POST请求参数异常返回示例】
在这里插入图片描述

【GET请求参数异常返回示例】

{"flag":false,"code":500,"message":"m1.number: 最小不能小于10","data":null}

三、效验注解

https://www.cnblogs.com/jinzlblog/p/16635043.html 注解

注解用法适用类型
@Null被注解的字段必须为空
@NotNull被注解的字段必须不为空
@NotBlank带注解的元素不能为null,并且必须至少包含一个非空白字符
@NotEmpty带注解的元素不能为null也不能为空String(长度)集合(大小)数组(长度)
@AssertTrue检查该字段必须为TrueBoolean
@AssertFalse检查该字段必须为FalseBoolean
@Min(value)被注解的字段必须大于等于指定的最小值
@Max(value)被注解的字段必须小于等于指定的最大值
@Negative带注解的元素必须是严格的负数(0被认为是无效值)BigDecimal,BigInteger,byte,short,int,long及其包装类
@NegativeOrZero带注解的元素必须是严格的负数或0BigDecimal,BigInteger,byte,short,int,long及其包装类
@Positive带注解的元素必须是严格的正数(0被认为是无效值)BigDecimal,BigInteger,byte,short,int,long及其包装类
@PositiveOrZero带注解的元素必须是严格的正数或0BigDecimal,BigInteger,byte,short,int,long及其包装类
@DecimalMin被注解的字段必须大于等于指定的最小值BigDecimal,BigInteger,byte,short,int,long及其包装类
@DecimalMax被注解的字段必须小于等于指定的最大值BigDecimal,BigInteger,byte,short,int,long及其包装类
@Size(min=,max=)被注解的字段的size必须在min和max之间,不需要判空字符串、数组、集合
@Digits(integer, fraction)被注解的字段必须在指定范围内,整数部分长度小于integer,小数部分长度小于fraction字符串、数组、集合
@Past被注解的字段必须是一个过去的日期时间
@PastOrPresent被注解的字段必须是过去的或现在的日期时间
@Future被注解的字段必须是一个将来的日期时间
@FutureOrPresent被注解的字段必须是现在的或将来的日期时间
@Email字符串必须是格式正确的电子邮件地址String
@Pattern(value)被注解的字段必须符合指定的正则表达式

四、高级使用

1.分组校验

在实际项目中,可能多个接口需要使用同一个 DTO 类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在 DTO 类的字段上加约束注解无法解决这个问题。因此,spring-validation支持了分组校验的功能,专门用来解决这类问题。

还是上面的例子,比如保存 User 的时候,UserId 是可空的,但是更新 User 的时候,UserId 的值必须存在,其它字段的校验规则在两种情况下一样。这个时候就需要使用分组校验

简单点说:就是根据设置的条件来执行参数校验,其实就是一个判断,筛选设置了分组的参数进行校验,不过在这里叫做分组

【声明分组】

// 保存的时候校验分组
public interface InsertValidGroup {
}

// 更新的时候校验分组
public interface UpdateValidGroup {
}

【实体类】

需要注意的是,这里面只有参数Id设置了分组,其它参数没有设置则不会进行校验

@Data
public class UserParam {

    // 代表在属于更新的时候,校验id
    @NotNull(groups = UpdateValidGroup.class)
    private Integer id;

    @NotNull
    private String userName;

    @Length(min = 6, max = 20, message = "长度范围为6~20")
    private String account;

    @Length(min = 6, max = 20)
    private String password;

}

【接口】

@RestController
@Validated
public class HelloController {

    @PostMapping(value = "/test3")
    public Object m3(@RequestBody @Validated(UpdateValidGroup.class) UserParam param) {
        System.out.println(param);
        return UUID.randomUUID() + "----" + param;
    }

}

【测试】

请求体id为空,则出现异常,请求体id存在,则正常执行
在这里插入图片描述

【其余参数设置分组】

如上所示只有设置了分组的参数才会校验,那么其它的参数怎么办呢,只能挨个写

@Data
public class UserParam {

    @NotNull(groups = UpdateValidGroup.class)
    private Integer id;

    // 其余的参数把全部的分组都设置上
    @NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
    private String userName;

    @NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
    @Length(min = 6, max = 20, message = "长度范围为6~20")
    private String account;

    @NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
    @Length(min = 6, max = 20)
    private String password;
}

2.嵌套校验

当我们实体类中某个字段是对象,这种情况下,可以使用嵌套校验

@Data
public class UserParam {

    @NotNull(groups = UpdateValidGroup.class)
    private Integer id;

    @NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
    private String userName;

    @NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
    @Length(min = 6, max = 20, message = "长度范围为6~20")
    private String account;

    @NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
    @Length(min = 6, max = 20)
    private String password;

    // 可以针对对象参数校验
    @NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
    @Valid
    private Job job;

    // 可以针对集合对象参数校验
    @NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
    @Valid
    private List<Job> jobs;

    @Data
    public static class Job {

        @NotNull(groups = UpdateValidGroup.class)
        @Min(value = 10, groups = UpdateValidGroup.class)
        private Long jobId;

        @NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
        private String jobName;

        @NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
        private String position;
    }

}

【接口情况】
在这里插入图片描述

3.集合校验

【参数类】

@Data
public class ValidList {

    // 集合校验
    @Valid
    @NotNull
    @Size(min = 3, max = 10)
    private List<String> list;

}

【接口】

@RestController
@Validated
public class HelloController {

    @PostMapping(value = "/test4")
    public Object m4(@RequestBody @Validated ValidList list) {
        System.out.println(list);
        return UUID.randomUUID() + "----" + list;
    }

}

【测试】
在这里插入图片描述

4.自定义校验

业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。

例如性别参数只能是0或者1,某个字段必须是{“aa”,“bb”,"cc}中的一个

【自定义注解】

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
// 这里需要注意标明校验类
@Constraint(validatedBy = {ProcessValidator.class})
public @interface CheckValid {

    String message() default "数据错误";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

【校验实现类】

需要实现ConstraintValidator接口,然后重写isValid(),验证该参数必须属于集合内

public class ProcessValidator implements ConstraintValidator<CheckValid, String> {

    private List<String> list = Arrays.asList("aa", "bb", "cc");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value != null) {
            if (list.contains(value)) {
                return true;
            }
        }
        return false;
    }

}

【实体类参数】

@Data
public class ForumParam {
	
    // 通过正则表达式验证
    @NotNull
    @Pattern(regexp = "^(男|女){1}$")
    private String sex;

    // 通过自定义注解校验
    @NotNull
    @CheckValid
    private String pms;

}

【接口】

@RestController
@Validated
public class HelloController {

    @PostMapping(value = "/test4")
    public Object m4(@RequestBody @Validated ForumParam param) {
        System.out.println(param);
        return UUID.randomUUID() + "----" + param;
    }

}

【测试】
在这里插入图片描述

5.编程式校验

上面的示例都是基于注解来实现自动校验的,在某些情况下,我们可能希望以编程方式调用验证。这个时候可以注入 javax.validation.Validator 对象,然后再调用其 api。

@Autowired
private javax.validation.Validator globalValidator;

// 编程式校验
@PostMapping(value = "/test5")
public Object m5(@RequestBody UserParam param) {
    Set<ConstraintViolation<UserParam>> set = validator.validate(param, UpdateValidGroup.class);
    // 如果校验通过,set;否则,set包含未校验通过项
    if (set.isEmpty()) {
        // 校验通过,才会执行业务逻辑处理
    } else {
        // 遍历出现异常的字段
        for (ConstraintViolation<UserParam> violation : set) {
            System.out.println(violation.getPropertyPath() + "---" + violation.getMessage());
        }
    }

    System.out.println(param);
    return UUID.randomUUID() + "----" + param;
}

6.快速失败

Spring Validation 默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启 Fali Fast 模式,一旦校验失败就立即返回。

package com.h3c.config;

import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

/**
 * @version JDK11
 * @author: wys4822
 * @date: 2022年09月07日
 */
@Configuration
public class ValidConfig {

    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                // 快速失败模式
                .failFast(true)
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }

}

【测试】
在这里插入图片描述

7.@Valid 和 @Validated 区别

image-20210525021620127

五、源码分析

1.@RequestBody参数校验实现原理

a>RequestResponseBodyMethodProcessor

Spring-MVC框架中,RequestResponseBodyMethodProcessor是用于解析 @RequestBody标注的参数以及处理 @ResponseBody 标注方法的返回值的。显然,执行参数校验的逻辑肯定就在解析参数的方法 resolveArgument() 中:

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
	
    @Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		parameter = parameter.nestedIfOptional();
         // 根据请求体转换参数
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
		String name = Conventions.getVariableNameForParameter(parameter);

		if (binderFactory != null) {
			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
			if (arg != null) {
                 // 执行参数校验
				validateIfApplicable(binder, parameter);
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
				}
			}
			if (mavContainer != null) {
				mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
			}
		}

		return adaptArgumentIfNecessary(arg, parameter);
	}
}

b>validateIfApplicable()

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
   // 获取参数前面设置的注解,注解中肯定会包含@RequestBody
   Annotation[] annotations = parameter.getParameterAnnotations();
   for (Annotation ann : annotations) {
      // 判断是否存在@Validated,通过一个工具类来实现
      Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
      if (validationHints != null) {
         binder.validate(validationHints);
         break;
      }
   }
}

在这里插入图片描述

c>determineValidationHints()

需要声明的是,在接口参数前面增加校验注解,注解可以为@Validated或者@Valid,2个都可以

@Valid注解判断存在即可通过判断,而@Validated注解判断存在后,还需要尝试获取里面的分组校验

public abstract class ValidationAnnotationUtils {

   private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];

   @Nullable
   public static Object[] determineValidationHints(Annotation ann) {
      Class<? extends Annotation> annotationType = ann.annotationType();
      String annotationName = annotationType.getName();
      // 通过注解路径判断是否为@Valid,原因是因为该注解有很多重名的
      if ("javax.validation.Valid".equals(annotationName)) {
         return EMPTY_OBJECT_ARRAY;
      }
      // 判断@Validated注解
      Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
      // 因为@Validated可以设置分组校验,所以这里需要获取value,封装成数组返回
      if (validatedAnn != null) {
         Object hints = validatedAnn.value();
         return convertValidationHints(hints);
      }
      if (annotationType.getSimpleName().startsWith("Valid")) {
         Object hints = AnnotationUtils.getValue(ann);
         return convertValidationHints(hints);
      }
      return null;
   }

   private static Object[] convertValidationHints(@Nullable Object hints) {
      if (hints == null) {
         return EMPTY_OBJECT_ARRAY;
      }
      return (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
   }

}

d>validate()

上面验证了参数是否标注了@Validated或者@Valid,代表该参数需要执行校验逻辑,那么接下来肯定就是遍历字段校验了,那么该validate()在上面的validateIfApplicable被调用

public void validate(Object... validationHints) {
   // 获取到参数对象
   Object target = getTarget();
   // 参数为空,就报错了
   Assert.state(target != null, "No target to validate");
   // 获取默认的绑定结果
   BindingResult bindingResult = getBindingResult();
   // 遍历校验
   for (Validator validator : getValidators()) {
      if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
         // 开启核心的校验
         ((SmartValidator) validator).validate(target, bindingResult, validationHints);
      }
      else if (validator != null) {
         validator.validate(target, bindingResult);
      }
   }
}

2.@RequestParam参数校验实现原理


{
   // 获取到参数对象
   Object target = getTarget();
   // 参数为空,就报错了
   Assert.state(target != null, "No target to validate");
   // 获取默认的绑定结果
   BindingResult bindingResult = getBindingResult();
   // 遍历校验
   for (Validator validator : getValidators()) {
      if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
         // 开启核心的校验
         ((SmartValidator) validator).validate(target, bindingResult, validationHints);
      }
      else if (validator != null) {
         validator.validate(target, bindingResult);
      }
   }
}

2.@RequestParam参数校验实现原理

文章来源:https://blog.csdn.net/weixin_44051038/article/details/135374132
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。