本篇文章介绍如何在springBoot中使用Hibernate Validate,如果优美的通过springBoot全局异常处理Hibernate Validate在参数校验失败后报出的异常
head.jpeg

导入依赖

如果是springBoot项目,那么spring-boot-starter-web中就已经依赖hibernate-validator

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-web</artifactId>
  4. </dependency>

注意springboot2.x后导入依赖需要手动

  1. <!--参数校验:在springboot2.x之后 web起步依赖中不再含有该依赖 需要手动在引入依赖-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-validation</artifactId>
  5. </dependency>

Validator的configuration(不是必须的)

由于默认情况下,Hibernate-validator使用的校验策略是依次校验,并且将不通过的结果保存,最后再统一抛出异常信息,但实际上,当校验出现第一个不满足情况的时候,就可以停止了(当然,如果选择全部验证完也是可以的),所以我们手动配置一下
配置的目的就是当第一个参数不匹配的时候就直接停止下面检验,直接返回

/*
Hibernate Validator的校验模式有两种情况:
        1.快速失败(验证失败一个,快速返回错误信息)
        2.普通模式(默认,会校验完所有的属性,然后返回所有的验证失败信息)
        我们一般使用第一种,也就是校验时候验证失败的时候就及时返回,SpringBoot需要开启配置*/
//hibernate validator快速失败模式
@Configuration
public class Jsr303Config {
    @Bean
    public Validator validator(){
        ValidatorFactory validatorFactory = Validation
                .byProvider( HibernateValidator.class )
                .configure()
                .failFast( true )// 将fail_fast设置为true即可,如果想验证全部,则设置为false或者取消配置即可
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}

由上很简单的已经在springBoot中配置了 Hibernate Validate

案例使用

案例需求:添加用户信息实现

1.首先我们建立VO对象

public class UserVo {
    @NotBlank(message = "用户名不能为空")
    private String name;

    @Max(value = 120, message = "年龄不能超过120岁")
    private int age;

    @NotNull
    @Size(min = 8, max = 20, message = "密码必须大于8位并且小于20位")
    private String password;

    @Email(message = "请输入符合格式的邮箱")
    private String email;

}

2.Controller类编写

@RestController
@RequestMapping("/users")
public class UserController {
   @PostMapping
    public User addUser(@Valid @RequestBody User user) {
        // 仅测试验证过程,省略其他的逻辑
        return user;
    }
}

注意:
@Valid注解 它和 @Validated 作用都是:通过注解能够使得验证生效,如果去除的话,可以看到验证逻辑并没有生效。@Validated是spring框架的注解,两者更 多区别网上都有很好的解释

3.检验失败报异常

通过上面的一个简单注解之后,验证的逻辑已经能够生效,然而,在测试的时候,可能会出现下面的情况

参数前加上@Valid或Spring的 @Validated注解,如果验证不通过会抛出BindException异常,
如果参数前有@RequestBody注解,验证错误会抛出MethodArgumentNotValidException异常,
并变成400(BAD_REQUEST)响应

{
    "timestamp": "2018-11-09T01:47:56.985+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": []
}

    /**
     * 验证不通过抛出 `MethodArgumentNotValidException`
     */
    @PostMapping("/user3")
    public R handle3(@Valid @RequestBody User user) {
        // ...
        return R.success();
    }

    /**
     * 验证不通过抛出 `BindException`
     */
    @PostMapping("/user4")
    public R handle4(@Valid User user) {
        // ...
        return R.success();
    }

以上是@RequestBody标注的参数的验证及错误异常
but
@PathVariable以及@RequestParam标注的入参(不生效),而事实上,这两种类型的操作也是非常常用的(也是需要对这两种类型进行验证,除了手动验证外,还有一种通用的解决方案,也是通过注解来实现)

@RestController
@RequestMapping("/users")
public class UserController {
    // .....

    @GetMapping("/{name}")
    public User getUserByName(
                    @NotNull 
                    @Size(min = 1, max = 20, message = "用户名格式有误")
                    @PathVariable String name) {
        User user = new User();
        user.setName(name);
        return user;
    }

    @GetMapping
    public User getUserByNameParam(
                    @NotNull 
                    @Size(min = 1, max = 20, message = "用户名格式有误") 
                    @RequestParam("name") String name) {
        User user = new User();
        user.setName(name);
        return user;
    }
}

为了让对应的注解生效,可以在类的上方使用@Validated进行标注,注意是标注在类上方,即

@RestController
@RequestMapping("/users")
@Validated
public class UserController {
    // ...
}

但此时如果验证失败,会抛出异常信息,而且,异常类型不是MethodArgumentNotValidException,而是ConstraintViolationException

所以总结一次:
1.参数前加上@Valid或Spring的 @Validated注解,如果验证不通过会抛出BindException异常,
2.如果参数前有@RequestBody注解,验证错误会抛出MethodArgumentNotValidException异常,
3.如果@PathVariable以及@RequestParam标注的入参在检验错误报ConstraintViolationException异常
后续都得对这些错误进行全局处理

4.手动处理检验错误

@RestController
@RequestMapping("/users")
public class UserController {
   @PostMapping
    public User addUser(@Valid @RequestBody User user, BindingResult bindingResult) {
         if (bindingResult.hasErrors()) {
            // 具体的处理逻辑,如封装错误信息等
            }
        return user;
    }
}

但是这种方式不是很优雅,因为对于每一个需要验证的方法,都需要进行这样的逻辑(虽然封装处理可以解决,但依旧每次需要手动调用以及加入BindingResult参数)

5.全局处理3种异常

配合我以前文《springBoot全局异常》文章

/**
     * 拦截表单参数校验
     * 处理Get请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常
     */
    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler({BindException.class})
    public ResultVo bindException(BindException e) {
        BindingResult bindingResult = e.getBindingResult();
        log.info("数据验证异常:{}", bindingResult.getFieldError().getDefaultMessage());
        return ResultVoUtil.error(2001,Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());
    }

    /**
     * 处理请求参数格式错误 @RequestParam上validate失败后抛出的异常是javax.validation.ConstraintViolationException
     */
    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(ConstraintViolationException.class)
    public ResultVo ConstraintViolationExceptionHandler(ConstraintViolationException e) {
        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
        ConstraintViolation<?> violation = violations.iterator().next();
        String message = violation.getMessage();
        log.info("数据验证异常:{}", message);
        return ResultVoUtil.error(2001,message);
    }
    /**
     * 拦截JSON参数校验
     * 处理请求参数格式错误 @RequestBody上validate失败后抛出的异常是MethodArgumentNotValidException异常
     */
    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultVo bindException(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        log.info("数据验证异常:{}", bindingResult.getFieldError().getDefaultMessage());
        return ResultVoUtil.error(2001,Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());
    }

6.测试

postman测试:

image.png
控制台输出:
image.png