spring-boot-starter-validation数据校验全局异常拦截处理

03-29 1184阅读 0评论

一、为什么使用Validation来验证参数

        通常我们在使用spring框架编写接口时,对于部分接口的参数我们要进行判空或者格式校验来避免程序出现异常。那是我们一般都是使用if-else逐个对参数进行校验。这种方法按逻辑来说也是没有问题的,同样也能实现预期效果。但是,这样的代码从可读性以及美观程序来看,是非常糟糕的。那么,我们就可以使用@valid注解来帮助我们优雅的校验参数。

二、如何使用Validation相关注解进行参数校验

  •  为实体类中的参数或者对象添加相应的注解;
  • 在控制器层进行注解声明,或者手动调用校验方法进行校验
  • 对异常进行处理;

    三、Validation类的相关注解及描述

    spring-boot-starter-validation数据校验全局异常拦截处理 第1张

    spring-boot-starter-validation数据校验全局异常拦截处理 第2张

    注意:

    实体类中添加 @Valid 相关验证注解,并在注解中添加出错时的响应消息。

    import javax.validation.Valid;
    import javax.validation.constraints.NotBlank;
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Pattern;
    @Data
    public class User {
        @NotBlank(message = "姓名不能为空")
        private String username;
        @NotBlank(message = "密码不能为空")
        @Length(min = 6, max = 16, message = "密码长度为6-16位")
        private String password;
        @Pattern(regexp = "0?(13|14|15|17|18|19)[0-9]{9}", message = "手机号格式不正确")
        private String phone;
        // 嵌套必须加 @Valid,否则嵌套中的验证不生效
        @Valid
        @NotNull(message = "userinfo不能为空")
        private UserInfo userInfo;
    }
    

    如果是嵌套的实体对象,并且也要校验该对象,则需要在最外层属性上添加 @Valid 注解

    此处只列出Validator提供的大部分验证约束注解,请参考hibernate validator官方文档了解其他验证约束注解和进行自定义的验证约束注解定义。

    @Validated和@Valid在嵌套验证功能上的区别:

    @Validated:用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。

    @Valid:用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。

    @Validated和@Valid的区别和使用,包括嵌套检验可以参考:
    https://blog.csdn.net/qq_27680317/article/details/79970590
    https://blog.csdn.net/qq_45151158/article/details/112349233?spm=1001.2014.3001.5501

    四、使用Validation API进行参数效验步骤

            对于GET请求的参数可以使用@validated注解配合上面相应的注解进行校验或者按照原先if-else方式进行效验.

    而对于POST请求,大部分是以表单数据即以实体对象为参数,可以使用@Valid注解方式进行效验(可以简单概括一下,如果接口使用实体类接收参数,那么要用@Valid注解该对象,并且在对象的各属性上添加上方表格里的注解;如果接口直接使用某个字段来接收参数,那么在该字段前添加表格里的注解即可,可参考下文代码)。如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理。

    spring-boot-starter-validation数据校验全局异常拦截处理 第3张

    五、Spring Validation的三种校验方式

    第一种(适用于生产):在Controller方法参数前加@Valid注解——校验不通过时直接抛异常,get请求直接在平面参数前添加相应的校验规则注解,使用这种的话一般结合统一异常处理进行处理,后面会主要介绍这种方式,可以直接看六部分。

    第二种:在Controller方法参数前加@Valid注解,参数后面定义一个BindingResult类型参数——执行时会将校验结果放进bindingResult里面,用户自行判断并处理。

     

    /**
     * 将校验结果放进BindingResult里面,用户自行判断并处理
     *
     * @param userInfo
     * @param bindingResult
     * @return
     */
    @PostMapping("/testBindingResult")
    public String testBindingResult(@RequestBody @Valid UserInfo userInfo, BindingResult bindingResult) {
        // 参数校验
        if (bindingResult.hasErrors()) {
            String messages = bindingResult.getAllErrors()
                    .stream()
                    .map(ObjectError::getDefaultMessage)
                    .reduce((m1, m2) -> m1 + ";" + m2)
                    .orElse("参数输入有误!");
            //这里可以抛出自定义异常,或者进行其他操作
            throw new IllegalArgumentException(messages);
        }
        return "操作成功!";
    }
    

    这里我们是直接抛出了异常,如果没有进行全局异常处理的话,接口将会返回如下信息:spring-boot-starter-validation数据校验全局异常拦截处理 第4张

    第三种:用户手动调用对应API执行校验——Validation.buildDefault ValidatorFactory().getValidator().validate(xxx)

    这种方法适用于校验任意一个有valid注解的实体类,并不仅仅是只能校验接口中的参数;

    这里我提取出一个工具类,如下

    import org.springframework.util.CollectionUtils;
    import javax.validation.ConstraintViolation;
    import javax.validation.Valid;
    import javax.validation.Validation;
    import java.util.Set;
    /**
     * 手动调用api方法校验对象
     */
    public class MyValidationUtils {
        public static void validate(@Valid Object value) {
            Set validateSet = Validation.buildDefaultValidatorFactory()
                    .getValidator()
                    .validate(value);
            if (!CollectionUtils.isEmpty(validateSet)) {
                String messages = validateSet.stream()
                        .map(ConstraintViolation::getMessage)
                        // 归约实现字符串合并
                        .reduce((m1, m2) -> m1 + ";" + m2)
                        .orElse("参数输入有误!");
                throw new IllegalArgumentException(messages);
            }
        }
    }
    

    六、Spring Boot项目中实战演练

     1、安装依赖:

        org.springframework.boot
        spring-boot-starter-validation
    
    
    2、自定义异常

    继承RuntimeException,要知道,spring 对于 RuntimeException 异常才会进行事务回滚,所以要继承RuntimeException。

    @Data
    @EqualsAndHashCode(callSuper = true)
    public class MyException extends RuntimeException {
        private Integer code;
        public MyException(ResultEnum resultEnum) {
            super(resultEnum.getMsg());
            this.code = resultEnum.getCode();
        }
        public MyException(Integer code, String msg) {
            super(msg);
            this.code = code;
        }
    }
    

    3、定义三个异常拦截器

    • ValidationExceptionHandle:校验异常统一拦截返回,定义在最前面
    • OtherExceptionHandle:其他异常拦截,用于项目中其他异常的拦截返回
    • FinalExceptionHandle:最终异常拦截,最后一道防线。

      @RestControllerAdvice是帮助我们把信息转成json格式返回

      @ResponseBody是将方法中的字符串转成json格式同一返回,一般该方法返回值为Object

      三个异常拦截器都使用order控制顺序,小的排在前面。
      注意:过滤器中的异常无法被拦截

      ValidationExceptionHandle.java,验异常统一拦截返回,定义在最前面

      @RestControllerAdvice
      @Order(80)
      @Slf4j
      public class ValidationExceptionHandle extends ResponseEntityExceptionHandler {
          @Override
          protected ResponseEntity handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
              logger.error(ex.getMessage());
              return validExceptionCommon(ex.getBindingResult());
          }
          @Override
          protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
              logger.error(ex.getMessage());
              return validExceptionCommon(ex.getBindingResult());
          }
          /**
           * 校验异常统一返回格式
           * @param result
           * @return
           */
          private ResponseEntity validExceptionCommon(BindingResult result){
              ResultVo resultVo = new ResultVo();
              if (result.hasErrors()) {
                  List errors = result.getAllErrors();
                  for (ObjectError error : errors) {
                      FieldError fieldError = (FieldError) error;
                      resultVo.setCode(500);
                      resultVo.setMsg(fieldError.getDefaultMessage());
                  }
              }
              return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(resultVo);
          }
      }
      

      OtherExceptionHandle.java,其他异常拦截,用于项目中其他异常的拦截返回。

      @ControllerAdvice
      @Order(90)
      public class OtherExceptionHandle {
          private Logger logger = LoggerFactory.getLogger(getClass());
          /**
           * 无
           * @param e
           * @return
           */
          @ExceptionHandler(ValidationException.class)
          @ResponseBody
          public Object handleValidationException(ValidationException e) {
              logger.error(e.getMessage(), e);
              return e;
          }
          /**
           * 违反约束异常处理
           * @param e
           * @return
           */
          @ExceptionHandler(ConstraintViolationException.class)
          @ResponseBody
          public Object handConstraintViolationException(ConstraintViolationException e) {
              logger.error(e.getMessage(), e);
              return e;
          }
      }
      

      FinalExceptionHandle.java,最终异常拦截,最后一道防线。

      @RestControllerAdvice
      @Order(100)
      public class FinalExceptionHandle{
          private final Logger logger = LoggerFactory.getLogger(getClass());
          @ExceptionHandler(value = Exception.class)
          @ResponseBody
          public Object  handle(Exception e) {
              logger.error(e.getMessage(), e);
              Map map = new HashMap();
              if (e instanceof MyException) {
                  MyException myException = (MyException) e;
                  map.put("code",500);
                  map.put("msg",myException.getMessage());
                  return map;
              } else {
                  e.printStackTrace();
                  map.put("code",500);
                  map.put("msg","出错啦");
                  return map;
              }
          }
      }
      

      七、ExceptionResolver与@ControllerAdvice的选择

              在基于Spring框架的项目中,可以通过在ApplicationContext-MVC.xml(即SpringMVC配置)文件中配置 ExceptionResolver 的bean ,来配置 全局捕获异常处理 类,然后自定义异常处理类处理。注意如果是spring配置文件中定义过的ExceptionResolver 类,不需要添加@Component。如果是SpringBoot 则需要。这是因为springboot没有自定义配置全局异常捕获类,所以需要添加@Component,来标识该类为Bean。

      异常处理可以分为三种:

      第一种是进入@Controller标识的方法前 产生的异常,例如URL地址错误。这种异常处理需要 异常处理类通过实现 ErrorController 来处理。
      第二种是进入Controller时,但还未进行逻辑处理时 产生的异常,例如传入参数数据类型错误。这种异常处理需要用@ControllerAdvice标识并处理,建议继承 ResponseEntityExceptionHandler 来处理,该父类包括了很多已经被@ExceptionHandler 注解标识的方法,包括一些参数转换,请求方法不支持等类型等等。
      第三种时进入Controller,进行逻辑处理时产生的异常,例如NullPointerException异常等。这种异常处理也可以用@ControllerAdvice来标识并进行处理,也建议继承ResponseEntityExceptionHandler 处理, 这里我们可以用@ExceptionHandler 自定义捕获的异常并处理。
      以上三种情况都是restful的情况,结果会返回一个Json。
      
      •  如果希望返回跳转页面,则需要实现HandlerExceptionResolver类来进行异常处理并跳转。
      • 注意@ControllerAdvice需要搭配@ExceptionHandler来使用,自定义捕获并处理异常。
      • @ControllerAdvice一样可以做页面跳转,返回String不要加@ResponseBody

        八、validation校验注解作用域

        • @Validated @Valid —— entity(实体)
        • @NotBlank —— String
        • @NotNull —— Integer
        • @NotEmpty —— java.util.Collection(集合)

          九、validation参数校验三种异常情况

          1、BindException

          BindException:作用于 @Validated @Valid 注解,仅对于表单提交有效,对于以json格式提交将会失效。

          /**
           * BindException异常处理
           * 

          BindException: 作用于@Validated @Valid 注解,仅对于表单提交有效,对于以json格式提交将会失效

          * * @param e BindException异常信息 * @return 响应数据 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(BindException.class) public Result bindExceptionHandler(BindException e) { String msg = e.getBindingResult().getFieldErrors() .stream() .map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage())) .reduce((x, y) -> String.format("%s; %s", x, y)) .orElse("参数输入有误"); log.error("BindException异常,参数校验异常:{}", msg); return Result.verifyError(msg); }
          2、MethodArgumentNotValidException

          MethodArgumentNotValidException:作用于 @Validated @Valid 注解,前端提交的方式为json格式有效,出现异常时会被该异常类处理。

          /**
           * MethodArgumentNotValidException-Spring封装的参数验证异常处理
           * 

          MethodArgumentNotValidException:作用于 @Validated @Valid 注解,接收参数加上@RequestBody注解(json格式)才会有这种异常。

          * * @param e MethodArgumentNotValidException异常信息 * @return 响应数据 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = MethodArgumentNotValidException.class) public Result methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getFieldErrors() .stream() .map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage())) .reduce((x, y) -> String.format("%s; %s", x, y)) .orElse("参数输入有误"); log.error("MethodArgumentNotValidException异常,参数校验异常:{}", msg); return Result.verifyError(msg); }
          3、ConstraintViolationException

          ConstraintViolationException:作用于 @NotBlank @NotNull @NotEmpty 注解,校验单个String、Integer、Collection等参数异常处理。

          /**
           * ConstraintViolationException-jsr规范中的验证异常,嵌套检验问题
           * 

          ConstraintViolationException:作用于 @NotBlank @NotNull @NotEmpty 注解,校验单个String、Integer、Collection等参数异常处理。

          *

          注:Controller类上必须添加@Validated注解,否则接口单个参数校验无效

          * * @param e ConstraintViolationException异常信息 * @return 响应数据 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = ConstraintViolationException.class) public Result constraintViolationExceptionHandler(ConstraintViolationException e) { String msg = e.getConstraintViolations() .stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining("; ")); log.error("ConstraintViolationException,参数校验异常:{}", msg); return Result.verifyError(msg); }

          注:Controller类上必须添加@Validated注解,否则接口参数校验无效

           

          4、统一异常处理完整代码

          Spring validation入参验证框架,一般在Controller类加上@Validated注解(可检验集合参数),接口方法对应的dto加上@Valid注解,然后直接对以上三个异常进行全局捕获处理即可。

          ValidationExceptionHandle.java完整代码:

           

          package com.tangsm.spring.boot.validation.handler;
          import com.tangsm.spring.boot.validation.domain.vo.Result;
          import org.slf4j.Logger;
          import org.slf4j.LoggerFactory;
          import org.springframework.core.annotation.Order;
          import org.springframework.http.HttpStatus;
          import org.springframework.validation.BindException;
          import org.springframework.web.bind.MethodArgumentNotValidException;
          import org.springframework.web.bind.annotation.ExceptionHandler;
          import org.springframework.web.bind.annotation.ResponseStatus;
          import org.springframework.web.bind.annotation.RestControllerAdvice;
          import javax.validation.ConstraintViolation;
          import javax.validation.ConstraintViolationException;
          import java.util.stream.Collectors;
          /**
           * 参数校验通用异常处理
           *
           * @author tangsm
           */
          @Order(80)
          @RestControllerAdvice
          public class ValidationExceptionHandle {
              private static final Logger log = LoggerFactory.getLogger(ValidationExceptionHandle.class);
              /**
               * BindException异常处理
               * 

          BindException: 作用于@Validated @Valid 注解,仅对于表单提交有效,对于以json格式提交将会失效

          * * @param e BindException异常信息 * @return 响应数据 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(BindException.class) public Result bindExceptionHandler(BindException e) { String msg = e.getBindingResult().getFieldErrors() .stream() .map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage())) .reduce((x, y) -> String.format("%s; %s", x, y)) .orElse("参数输入有误"); log.error("BindException异常,参数校验异常:{}", msg); return Result.verifyError(msg); } /** * MethodArgumentNotValidException-Spring封装的参数验证异常处理 *

          MethodArgumentNotValidException:作用于 @Validated @Valid 注解,接收参数加上@RequestBody注解(json格式)才会有这种异常。

          * * @param e MethodArgumentNotValidException异常信息 * @return 响应数据 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = MethodArgumentNotValidException.class) public Result methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getFieldErrors() .stream() .map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage())) .reduce((x, y) -> String.format("%s; %s", x, y)) .orElse("参数输入有误"); log.error("MethodArgumentNotValidException异常,参数校验异常:{}", msg); return Result.verifyError(msg); } /** * ConstraintViolationException-jsr规范中的验证异常,嵌套检验问题 *

          ConstraintViolationException:作用于 @NotBlank @NotNull @NotEmpty 注解,校验单个String、Integer、Collection等参数异常处理。

          *

          注:Controller类上必须添加@Validated注解,否则接口单个参数校验无效

          * * @param e ConstraintViolationException异常信息 * @return 响应数据 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = ConstraintViolationException.class) public Result constraintViolationExceptionHandler(ConstraintViolationException e) { String msg = e.getConstraintViolations() .stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining("; ")); log.error("ConstraintViolationException,参数校验异常:{}", msg); return Result.verifyError(msg); } }

          参考:

          springboot全局的异常拦截处理_springboot 全局异常拦截_L-960的博客-CSDN博客
          原创:全局异常捕获BindException、MethodArgumentNotValidException和ConstraintViolationException @Validated@Valid_bindexception没有getbindingresult方法_HD243608836的博客-CSDN博客
          高效使用hibernate-validator校验框架 - hjzqyx - 博客园 (cnblogs.com)
          SpringBoot 参数校验的方法 - 木白的菜园 - 博客园 (cnblogs.com)


免责声明
1、本网站属于个人的非赢利性网站,转载的文章遵循原作者的版权声明。
2、本网站转载文章仅为传播更多信息之目的,凡在本网站出现的信息,均仅供参考。本网站将尽力确保所
提供信息的准确性及可靠性,但不保证信息的正确性和完整性,且不对因信息的不正确或遗漏导致的任何
损失或损害承担责任。
3、任何透过本网站网页而链接及得到的资讯、产品及服务,本网站概不负责,亦不负任何法律责任。
4、本网站所刊发、转载的文章,其版权均归原作者所有,如其他媒体、网站或个人从本网下载使用,请在
转载有关文章时务必尊重该文章的著作权,保留本网注明的“稿件来源”,并白负版权等法律责任。

手机扫描二维码访问

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
评论列表 (暂无评论,1184人围观)

还没有评论,来说两句吧...

目录[+]