前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >springboot validation参数校验

springboot validation参数校验

作者头像
山行AI
发布2019-08-26 16:26:15
3.7K0
发布2019-08-26 16:26:15
举报
文章被收录于专栏:山行AI山行AI

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

常规使用方式

  1. 引入pom
代码语言:javascript
复制
<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的依赖。

2. 在bean上直接使用注解:

代码语言:javascript
复制
@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;

3. 在controller中添加@Valid注解

代码语言:javascript
复制
@PostMapping(value = "/save")
    @RequiresPermissions("medic:add")
    @AddSysLog(descrption = "保存药品信息")
    @LoginedUser
    @ApiOperation(value = "保存药品信息",notes = "新增药品相关接口")
    public R save(@CurrentUser@ApiIgnore SysUser sysUser, @RequestBody @Valid Medicine medicine) {

4. 普通的String 类型的

代码语言:javascript
复制
@PostMapping(value = "/save")
    @RequiresPermissions("medic:add")
    @AddSysLog(descrption = "查询药品相关接口")
    @LoginedUser
    @ApiOperation(value = "查询药品相关接口",notes = "查询药品相关接口")
    public R queryByName(@CurrentUser@ApiIgnore SysUser sysUser, @NotNull(message = "查询条件不能为空") String medicineName) {

5. 如需要国际化

代码语言:javascript
复制
@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中

代码语言:javascript
复制
medicine.message.notnull=药品名称不能为空

在messageenUS.properties中

代码语言:javascript
复制
medicine.message.notnull=medicine name can not be null

6. 默认使用spring validator如使用hibernate validator:

代码语言:javascript
复制
@Configuration
public class ValidatorConfig {

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

7. 异常统一捕获处理,省去每个@Valid后都跟着处理BindingResult

代码语言:javascript
复制
/**
     * 数据校验处理
     * @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);
    }

8. spring validator分组处理 为什么要有分组这一说呢?因为,举个例子,添加的时候不需要校验id,而修改的时候id不能为空,有了分组以后,就可以添加的时候校验用组A,修改的时候校验用组B。 两个分组的接口,一个是添加的组,一个是修改的组:

实体类中:

controller中:

代码语言:javascript
复制
@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. 创建注解
  1. 创建validator
  1. 使用方式 在需要校验的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():

代码语言:javascript
复制
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。

代码语言:javascript
复制
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()方法的最后一行:

代码语言:javascript
复制
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);

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

代码语言:javascript
复制
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:

代码语言:javascript
复制
@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异常处理器:

代码语言:javascript
复制
@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

代码语言:javascript
复制
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 容器中获取校验器对象,注入使用。
  • 关于校验模式,默认会校验完所有属性,然后将错误信息一起返回,但很多时候不需要这样,一个校验失败了,其它就不必校验了
代码语言:javascript
复制
@Configuration
public class ValidatorConfig {

    @Bean
    public Validator validator(){
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}
  • 注解的使用方式
代码语言:javascript
复制
@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
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开发架构二三事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 常规使用方式
  • 注意点
    • 抛出BindException的地方
      • 抛出ConstraintViolationException的地方
        • 关于MethodArgumentNotValidException异常的抛出
        • 参考
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档