springboot validation参数校验

springboot validation参数校验

Bean Validation 为 JavaBean 验证定义了相应的元数据模型和 API。缺省的元数据是 Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。在应用程序中,通过使用 Bean Validation 或是你自己定义的 constraint,例如 @NotNull, @Max, @ZipCode, 就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraint。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。

常规使用方式

1.引入pom

<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>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

其中在spring-boot-starter-web中有hibernate-validater的依赖。

  1. 在bean上直接使用注解:
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Medicine implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 序号,药品编号
     */
    private Long medicineCode;

    /**
     * 简码
     */
    @NotBlank(message = "拼音简码不能为空")
    private String simpleCode;

    /**
     * 条码
     */
    @NotBlank(message = "商品条码不能为空")
    private String tiaoCode;

    /**
     * 药品名称
     */
    @NotBlank(message = "药品名称不能为空")
    private String medicineName;

    /**
     * 剂型
     */
    @NotBlank(message = "剂型不能为空")
    private String jxType;

    /**
     * 通用名
     */
    private String commonName;

    /**
     * 规格
     */
    private String guiType;

    /**
     * 生产厂家
     */
    private String productor;

    /**
     * 批准文号
     */
    private String permitCode;

    /**
     * 包装单位
     */
    @NotBlank(message = "包装单位不能为空")
    private String wrapUnit;

    /**
     * 最小单位
     */
    @NotBlank(message = "最小单位不能为空")
    private String minUnit;
  1. 在controller中添加@Valid注解
@PostMapping(value = "/save")
    @RequiresPermissions("medic:add")
    @AddSysLog(descrption = "保存药品信息")
    @LoginedUser
    @ApiOperation(value = "保存药品信息",notes = "新增药品相关接口")
    public R save(@CurrentUser@ApiIgnore SysUser sysUser, @RequestBody @Valid Medicine medicine) {
  1. 普通的String 类型的
@PostMapping(value = "/save")
    @RequiresPermissions("medic:add")
    @AddSysLog(descrption = "查询药品相关接口")
    @LoginedUser
    @ApiOperation(value = "查询药品相关接口",notes = "查询药品相关接口")
    public R queryByName(@CurrentUser@ApiIgnore SysUser sysUser, @NotNull(message = "查询条件不能为空") String medicineName) {
  1. 如需要国际化
@PostMapping(value = "/save")
    @RequiresPermissions("medic:add")
    @AddSysLog(descrption = "查询药品相关接口")
    @LoginedUser
    @ApiOperation(value = "查询药品相关接口",notes = "查询药品相关接口")
    public R queryByName(@CurrentUser@ApiIgnore SysUser sysUser, @NotNull(message = "medicine.message.notnull") String medicineName) {

在messagezhCN.properties中

medicine.message.notnull=药品名称不能为空

在messageenUS.properties中

medicine.message.notnull=medicine name can not be null
  1. 默认使用spring validator如使用hibernate validator:
@Configuration
public class ValidatorConfig {

    @Bean
    public Validator validator(){
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure()
        .failFast(true).buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}
  1. 异常统一捕获处理,省去每个@Valid后都跟着处理BindingResult
/**
     * 数据校验处理
     * @param e
     * @return
     */
    @ExceptionHandler({BindException.class, ConstraintViolationException.class})
    public String validatorExceptionHandler(Exception e) {
        String msg = e instanceof BindException ? msgConvertor(((BindException) e).getBindingResult())
                : msgConvertor(((ConstraintViolationException) e).getConstraintViolations());

        return msg;
    }

    /**
     * 参数不合法异常
     *
     * @param ex
     * @return
     * @Description
     * @author
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public R handleException(MethodArgumentNotValidException ex) {
        BindingResult a = ex.getBindingResult();
        List<ObjectError> list = a.getAllErrors();
        String errorMsg = ErrorEnum.PARAMETER_ERR.getErrorMsg();
        if (CollectionUtils.isNotEmpty(list)) {
            errorMsg = list.get(0).getDefaultMessage();
        }
        return R.error(ErrorEnum.PARAMETER_ERR.getErrorCode(),errorMsg);
    }
  1. spring validator分组处理 为什么要有分组这一说呢?因为,举个例子,添加的时候不需要校验id,而修改的时候id不能为空,有了分组以后,就可以添加的时候校验用组A,修改的时候校验用组B。 两个分组的接口,一个是添加的组,一个是修改的组:

在这里插入图片描述
在这里插入图片描述
实体类中:
在这里插入图片描述
controller中:

@PostMapping(value = "/update")
    @RequiresPermissions("medic:update")
    @AddSysLog(descrption = "修改药品信息")
    @ApiOperation(value = "修改药品信息",notes = "修改药品相关接口")
//    @ApiImplicitParams({
//            @ApiImplicitParam(name = "medicine", value = "药品json数据", required = true, paramType = "body", dataType = "medicine")})
    @LoginedUser
    public R update(@CurrentUser@ApiIgnore SysUser sysUser,@RequestBody @Validated(value = {MedicineGroupEdit.class}) Medicine medicine) {

见:https://docs.spring.io/spring/docs/5.0.5.RELEASE/spring-framework-reference/core.html#validation

9.hibernate validator自定义validator 见:https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-customconstraints
自定义方法:

创建注解
在这里插入图片描述

  1. 创建validator
    在这里插入图片描述

使用方式 在需要校验的bean上添加:
在这里插入图片描述
注意点

JSR 303 – Bean Validation 规范 http://jcp.org/en/jsr/detail?id=303
Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。如果想了解更多有关 Hibernate Validator 的信息,请查看 http://www.hibernate.org/subprojects/validator.html
一个 constraint 通常由 annotation 和相应的 constraint validator 组成,它们是一对多的关系。也就是说可以有多个 constraint validator 对应一个 annotation。在运行时,Bean Validation 框架本身会根据被注释元素的类型来选择合适的 constraint validator 对数据进行验证
BindingResult必须跟在被校验参数之后,若被校验参数之后没有BindingResult对象,将会抛出BindException
不要使用 BindingResult 接收String等简单对象的错误信息(也没有特别的错,只是 result 是接不到值。)。简单对象校验失败,会抛出 ConstraintViolationException。
SpringMVC 在进行方法参数的注入(将 Http请求参数封装成方法所需的参数)时,不同的对象使用不同的解析器注入对象。注入实体对象时使用ModelAttributeMethodProcessor而注入 String 对象使用AbstractNamedValueMethodArgumentResolver。而正是这个差异导致了BindingResult无法接受到简单对象(简单的入参参数类型)的校验信息。

HandlerMethodArgumentResolverComposite#resolveArgument():

public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        // 获取 parameter 参数的解析器
        HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
        // 调用解析器获取参数
        return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    }

    // 获取 parameter 参数的解析器
    private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
        // 从缓存中获取参数对应的解析器
        HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
        for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
            // 解析器是否支持该参数类型
            if (methodArgumentResolver.supportsParameter(parameter)) {
                result = methodArgumentResolver;
                this.argumentResolverCache.put(parameter, result);
                break;
            }
        }
        return result;
    }

注入 String 参数时,在AbstractNamedValueMethodArgumentResolver#resolveArgument()中,不会抛出BindException/ConstraintViolationException异常、也不会将 BindingResult 传入到方法中。

抛出BindException的地方
注入对象时在ModelAttributeMethodProcessor#resolveArgument():154 行的 validateIfApplicable(binder, parameter)语句,进行了参数校验,校验不通过并且实体对象后不存在BindingResult对象,则会在this#resolveArgument():156抛出BindException。

public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        // bean 参数绑定和校验
        WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);

        // 参数校验
        validateIfApplicable(binder, parameter);
        // 校验结果包含错误,并且该对象后不存在 BindingResult 对象,就抛出异常
        if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
            throw new BindException(binder.getBindingResult());
        }

        // 在对象后注入 BindingResult 对象
        Map<String, Object> bindingResultModel = bindingResult.getModel();
        mavContainer.removeAttributes(bindingResultModel);
        mavContainer.addAllAttributes(bindingResultModel);
    }

抛出ConstraintViolationException的地方
InvocableHandlerMethod#invokeForRequest()的doInvoke(args)方法中Mehtod.invoke() 对应的CglibAopProxy$CglibMethodInvocation的父类ReflectiveMethodInvocation,在 ReflectiveMethodInvocation#process()方法的最后一行:

return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);

这里的 Methodnterceptor 接口的真身是 MethodValidationInterceptor:

public Object invoke(MethodInvocation invocation) throws Throwable {
        ExecutableValidator execVal = this.validator.forExecutables();
        // 校验参数
        try {
            result = execVal.validateParameters(
                    invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        catch (IllegalArgumentException ex) {
            // 解决参数错误异常、再次校验
            methodToValidate = BridgeMethodResolver.findBridgedMethod(
                    ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
            result = execVal.validateParameters(
                    invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }

        // 执行结果
        Object returnValue = invocation.proceed();

        // 校验返回值
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }

        return returnValue;
    }

关于MethodArgumentNotValidException异常的抛出
通常采取处理BindException:

@RestControllerAdvice
@Slf4j
public class ExceptionAdvice {

    @ExceptionHandler(BindException.class)
    public Object validExceptionHandler(BindException e){
        FieldError fieldError = e.getBindingResult().getFieldError();
        assert fieldError != null;
        log.error(fieldError.getField() + ":" + fieldError.getDefaultMessage());
        // 将错误的参数的详细信息封装到统一的返回实体
        ...
        return ...;
    }
}

但是, 如果你使用了@RequestBody @Valid 来封装参数并校验, 这个时候这个异常处理器又不起作用了,需要添加MethodArgumentNotValidException异常处理器:

@ExceptionHandler(MethodArgumentNotValidException.class)
     @ResponseBody
     public R handleException(MethodArgumentNotValidException ex) {
         BindingResult a = ex.getBindingResult();
         List<ObjectError> list = a.getAllErrors();
         String errorMsg = ErrorEnum.PARAMETER_ERR.getErrorMsg();
         if (CollectionUtils.isNotEmpty(list)) {
             errorMsg = list.get(0).getDefaultMessage();
         }
         return R.error(ErrorEnum.PARAMETER_ERR.getErrorCode(),errorMsg);
     }

原因见 https://github.com/spring-projects/spring-framework/issues/14790

These are actually intentionally different exceptions. @ModelAttribute, which is assumed by default if no other annotation is present, 
goes through data binding and validation, and raises BindException to indicate a failure with binding request properties or validating
the resulting values. @RequestBody, on the other hand converts the body of the request via HttpMessageConverter, validates it and raises
various conversion related exceptions or a MethodArgumentNotValidexception if validation fails. 
In most cases a MethodArgumentNotValidException can be handled generically (e.g. via @ExceptionHandler method) while BindException is very
often handled individually in each controller method.

若没有手动配置Validator对象,自然需要从 Spring 容器中获取校验器对象,注入使用。
关于校验模式,默认会校验完所有属性,然后将错误信息一起返回,但很多时候不需要这样,一个校验失败了,其它就不必校验了

@Configuration
public class ValidatorConfig {

    @Bean
    public Validator validator(){
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}

注解的使用方式

@Null
被注释的元素必须为 null

@NotNull
被注释的元素必须不为 null

@AssertTrue
被注释的元素必须为 true

@AssertFalse
被注释的元素必须为 false

@Min(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@Max(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@DecimalMin(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@DecimalMax(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@Size(max=, min=)
被注释的元素的大小必须在指定的范围内

@Digits(integer, fraction)
被注释的元素必须是一个数字,其值必须在可接受的范围内

@Past
被注释的元素必须是一个过去的日期

@Future
被注释的元素必须是一个将来的日期

@Pattern(regex=, flag=)
被注释的元素必须符合指定的正则表达式

@NotBlank(message =)
验证字符串非null,且长度必须大于0

以下为hibernate Validator附加的
@Email
被注释的元素必须是电子邮箱地址

@Length(min=, max=)
被注释的字符串的大小必须在指定的范围内

@NotEmpty
被注释的字符串的必须非空

@Range(min=, max=, message=)
被注释的元素必须在合适的范围内

@Valid与@Validated的区别:
在这里插入图片描述

上面图片来源自https://www.jianshu.com/p/2432d0f51c0e,其他区别见:https://blog.csdn.net/qq_27680317/article/details/79970590

参考
https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/
https://www.jianshu.com/p/2432d0f51c0e
https://www.cnblogs.com/cjsblog/p/8946768.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值