Spring Boot使用Hibernate validator进行参数校验

原文地址,转载请注明出处: https://blog.csdn.net/qq_34021712/article/details/87545287  ©王赛超

Spring Boot已经可以帮助我们快速构建一个web服务,针对请求的参数如果只在客户端进行数据有效性验证都不是安全有效的,我们可以进行抓包直接调用接口,所以我们需要在客户端做参数校验,同时后端服务也需要对参数进行校验才可以。所以经常会看到类似下面这样的代码:
在这里插入图片描述

你会发现整个方法才100行,其中80行都是参数校验,大量的参数校验代码充斥在业务代码中,看着很不爽,而且不少浪费时间,本篇将帮助你使用SpringBoot实现RESTAPI服务的有效验证。

什么是spring-boot-starter-validation?

我们都知道 Spring BootStarters 机制,只要导入相应的包,我们就可以快速的使用对应的api,所以我们只要加入 spring-boot-starter-validation 这个 Starter ,就可以使用其实现验证。那什么是 spring-boot-starter-validation

spring-boot-starter-validation 就是使用 Hibernate Validator 框架来提供 Java Bean 验证功能。

看到 Hibernate 开头,大家首先想到的就是 Hibernate ORM 框架,Hibernate ValidatorHibernate项目中的一个数据校验框架,是 JSR 380 参考实现。Hibernate ValidatorBean Validation APITCK 都是使用了Apache Software License 2.0

Hibernate Validator 6Bean Validation 2.0 需要 Java8 或更新版本。

英文文档地址:
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single

Spring Boot中使用spring-boot-starter-validation

pom添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
参数校验demo

配置要验证的请求实体

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

    /** 身份证号 */
    @Pattern(regexp="^(\\d{18}|\\d{17}(\\d{1}|[X|x]))$",message="身份证格式不正确")
    private String idCardNum;

    /** 年龄 */
    @NotNull(message="用户年龄不能为空")
    @Min(value = 1,message = "年龄不正确")
    @Max(value = 150,message = "年龄不正确")
    private Integer age;

    /** 家庭住址 */
    @NotBlank(message = "用户地址不能为空")
    private String address;

    /** 是否已确认 */
    @NotNull(message="用户必须确认")
    @AssertTrue(message = "用户必须确认")
    private Boolean confirmResult;

    //省略 get set 方法
}

Controller方法配置

@RequestMapping(value = "/createUser",method = RequestMethod.POST)
@ResponseBody
public void createUser(@RequestBody @Valid ModelCreateUserRequest modelCreateUserRequest,BindingResult result){

    if(result.hasErrors()){
        for (ObjectError error : result.getAllErrors()) {
            System.out.println(error.getDefaultMessage());
        }
    }

}

POST请求传入的参数:
{"name":"","idCardNum":"","age":"","address":"","confirmResult":""}

输出结果:

用户年龄不能为空
用户姓名不能为空
用户地址不能为空
身份证格式不正确
用户必须确认

使用方式非常简单,在需要绑定的bean上直接添加注解,然后在 @RequestBody ModelCreateUserRequest modelCreateUserRequest之间加 @Valid注解,将校验结果放到BindingResult对象中。

如果要检验的参数类型不能使用某个注解校验的时候,就会报异常,例如 在Integer类型上使用 Pattern 注解,就会报下面的异常,Integer类型无法使用 Pattern 注解来校验:

javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint ‘javax.validation.constraints.Pattern’ validating type ‘java.lang.Integer’. Check configuration for ‘age’。

Hibernate Validator的校验模式

上面的demo中,一次性打印了所有验证不符合规则的属性,通常按顺序验证到第一个字段不符合验证要求时,就可以直接拒绝请求了。Hibernate Validator有以下两种验证模式:

普通模式(默认模式)
普通模式会校验完所有的属性,然后返回所有的验证失败信息。这也是默认使用的模式。

快速失败
Hibernate Validator也支持在第一次发生约束违规时立即从当前验证返回。

两种配置方式

官网地址:
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-provider-specific-settings
第一种:设置Hibernate Validator特定选项

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .failFast( true )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

第二种:可以通过Configuration#addProperty()传递特定选项

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .addProperty( "hibernate.validator.fail_fast", "true" )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

Spring Boot 配置hibernate Validator为快速失败模式:

@Configuration
public class ValidatorConfiguration {

    @Bean
    public Validator validator(){

        ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
                .configure()
                .failFast(true)
                .buildValidatorFactory();
        Validator validator = validatorFactory.getValidator();

        return validator;
    }

}

捕获全局参数校验异常并返回错误提示信息

上面我在绑定请求参数的bean后面 添加了 BindindResult 参数,然后在方法体内处理不符合规则的属性,并响应给客户端,但是如果在每个方法体内,都写一遍处理逻辑又是很麻烦的操作,所以我们可以使用下面的操作。

首先将 参数bean后面的 BindindResult 去掉,这个时候,再请求将会抛出 org.springframework.web.bind.MethodArgumentNotValidException

后面我们要讲的: 直接对方法内的单个添加了 @RequestParam 参数校验时,抛出的是 javax.validation.ConstraintViolationException

我们可以捕获这两个异常,在这里进行统一处理,并返回。例如:

@ControllerAdvice
@Component
public class GlobalExceptionHandlerAdvice {

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ReturnResult handleConstraintViolationException(ConstraintViolationException exception) {

        Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();

        StringBuilder stringBuilder = new StringBuilder();
        for (ConstraintViolation<?> item : violations) {
            //将所有的异常信息封装成String
            stringBuilder.append(item.getMessage()).append(" ");
        }

        ReturnResult returnResult = new ReturnResult();
        returnResult.setCode(HttpStatus.BAD_REQUEST.value());
        returnResult.setMessage(stringBuilder.toString());

        return returnResult;
    }

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ReturnResult handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {

        BindingResult bindingResult = exception.getBindingResult();

        List<FieldError> fieldErrors = bindingResult.getFieldErrors();

        StringBuilder stringBuilder = new StringBuilder();
        for (FieldError item : fieldErrors) {
            //将所有的异常信息封装成String
            stringBuilder.append(item.getDefaultMessage()).append(" ");
        }

        ReturnResult returnResult = new ReturnResult();
        returnResult.setCode(HttpStatus.BAD_REQUEST.value());
        returnResult.setMessage(stringBuilder.toString());

        return returnResult;
    }

}

@RequestParam注解使用Validator校验

上面使用的@RequestBody 注解,将请求参数绑定到bean上,但是有的情况下,我们的参数可能只是一个简单的数字或者字符串(采用@RequestParam注解),如果像上面那样将参数包装成对象甚是麻烦,下面我们看一下,怎么样才能使@RequestParam注解的参数也可以使用Spring Validator
配置MethodValidationPostProcessor

@Configuration
public class ValidatorConfiguration {

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
        //设置validator模式为快速失败
        postProcessor.setValidator(validator());
        return postProcessor;
    }

    @Bean
    public Validator validator(){

        ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
                .configure()
                .failFast(true)
                .buildValidatorFactory();
        Validator validator = validatorFactory.getValidator();

        return validator;
    }

}

Controller类上添加@Validated , 然后在方法参数内使用注解校验。

@Controller
@Validated
public class TestValidationController {

    @RequestMapping(value = "/demo2",method = RequestMethod.GET)
    @ResponseBody
    public void demo2(@NotBlank(message = "用户姓名不能为空") @RequestParam String name,
                      @NotNull(message = "年龄不正确") @Range(min = 1,max = 150,message = "年龄不正确") @RequestParam Integer age){

        System.out.println(name);
        System.out.println(age);

    }

}

到此,基本已经可以满足项目中的使用需求了,下面介绍一下其他的常用方法。

其他使用方式

1.在方法内对Model类校验

Model类 我们还使用上面的 ModelCreateUserRequest , 在 Spring Bean中通过 注入Validator 来进行验证。

@Autowired
private Validator validator;

@RequestMapping(value = "/demo3",method = RequestMethod.GET)
@ResponseBody
public void demo3(){
    ModelCreateUserRequest modelCreateUserRequest = new ModelCreateUserRequest();
    modelCreateUserRequest.setName("王赛超");
    modelCreateUserRequest.setIdCardNum("123456789012345678");
    modelCreateUserRequest.setAge(0);

    Set<ConstraintViolation<ModelCreateUserRequest>> validate = validator.validate(modelCreateUserRequest);
    for (ConstraintViolation<ModelCreateUserRequest> model : validate) {
        System.out.println(model.getMessage());
    }

}
2.对象级联校验

对象的内部包含另一个对象作为属性,在该属性上添加@Valid ,例如 A类中 包含B类 在B属性上添加 @Valid,校验A 也可以校验B

public class A {

    @NotBlank(message = "用户姓名不能为空")
    private String name;

    @NotNull(message="用户年龄不能为空")
    @Min(value = 1,message = "年龄不正确")
    @Max(value = 150,message = "年龄不正确")
    private Integer age;

    @NotNull(message = "B不存在")
    @Valid
    private B b;
}

public class B {

    @Range(min = 1000000,message = "你不能没有钱")
    private double money;
}

进行级联校验

@Autowired
private Validator validator;

@RequestMapping(value = "/demo4",method = RequestMethod.GET)
@ResponseBody
public void demo4(){

    A a = new A();
    a.setAge(25);
    a.setName("王赛超");

    B b = new B();
    a.setB(b);

    Set<ConstraintViolation<A>> validate = validator.validate(a);
    for (ConstraintViolation<A> model : validate) {
        System.out.println(model.getMessage());
    }

}
3.分组校验

有的时候,我们对一个实体类需要有多种验证方式,在不同的情况下使用不同验证方式,比如 用户的新增 和 修改 操作,唯一的区别就是 插入的时候没有 userid 属性,而修改的时候有userid
GroupA、GroupB:

public interface GroupA extends Default {
}

public interface GroupB extends Default {
}

Model类

public class UserInfo {

    /** 主键 */
    @NotBlank(message = "用户id不存在",groups = {GroupA.class})
    private Integer id;

    /** 姓名 */
    @NotBlank(message = "用户姓名不能为空",groups = {GroupA.class, GroupB.class})
    private String name;

    /** 身份证号 */
    @Pattern(regexp="^(\\d{18}|\\d{17}(\\d{1}|[X|x]))$",message="身份证格式不正确",groups = {GroupA.class, GroupB.class})
    private String idCardNum;

    /** 年龄 */
    @NotNull(message="用户年龄不能为空",groups = {GroupA.class, GroupB.class})
    @Range(min = 1,max = 150,message = "年龄必须在[1,150]",groups = {GroupA.class, GroupB.class})
    private Integer age;

    /** 家庭住址 */
    @NotBlank(message = "用户地址不能为空",groups = {GroupA.class, GroupB.class})
    private String address;

    /** 状态 0 可用 1 不可用 */
    @NotBlank(message = "用户状态不能为空",groups = {Default.class})
    private String state;
}

如上 UserInfo 所示,3个分组分别验证字段如下:

GroupA验证字段 id,name,idCardNum,age,address,state
GroupB验证字段 name,idCardNum,age,address,state
Default验证字段 state (DefaultValidator自带的默认分组)。

在controler中的代码如下:

@RequestMapping(value = "/save",method = RequestMethod.POST)
@ResponseBody
public void save(@RequestBody @Validated( { GroupA.class })UserInfo userInfo){
    System.out.println(userInfo.toString());
}

@RequestMapping(value = "/update",method = RequestMethod.POST)
@ResponseBody
public void update(@RequestBody @Validated( { GroupB.class })UserInfo userInfo){
    System.out.println(userInfo.toString());
}

验证GroupA
请求save方法,参数为:
{"id":"","name":"","idCardNum":"","age":"","address":"","state":""}

{
    "code": 400,
    "message": "用户年龄不能为空 身份证格式不正确 用户id不存在 用户地址不能为空 用户状态不能为空 用户姓名不能为空 ",
    "data": null
}

验证GroupB
请求update方法,参数为:
{"id":"","name":"王赛超","idCardNum":"123456789012345678","age":"25","address":"河北邯郸","state":"0"}
控制台打印:
UserInfo(id=null, name=王赛超, idCardNum=123456789012345678, age=25, address=河北邯郸, state=0)

4.定义组序列

默认情况下,无论它们属于哪个组,都不会按特定顺序计算约束。但是,在某些情况下,控制约束的评估顺序很有用。

例如你要开车,首先你要通过驾照的检测,你才可以开车,为了实现这样的验证顺序,您只需要定义一个接口并对其添加@GroupSequence,定义必须验证组的顺序 ,这样前面组验证不通过的,后面组不进行验证。

@GroupSequence({ GroupA.class, GroupB.class, Default.class })
public interface OrderedChecks {
}

在controler中的代码如下:

@RequestMapping(value = "/demo7",method = RequestMethod.POST)
@ResponseBody
public void demo7(@RequestBody @Validated( { OrderedChecks.class })UserInfo userInfo){
    System.out.println(userInfo.toString());
}

后面我们会学习,如何自定义校验方式。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值