SpingBoot项目使用@Validated和@Valid参数校验

一、什么是参数校验?

咱们在后端开发中,经常遇到的一个问题便是入参校验。简略来说便是对一个办法入参的参数进行校验,看是否符合咱们的要求。比方入参要求是一个金额,你前端没做约束,用户随便过来一个负数,或许一个字母,那么咱们的接口就会报错。

所以,一般咱们需求在办法的开始处,对入参的参数进行校验,不符合要求就报错回来,不往下进行。这个校验的进程便是参数校验。


二、为什么需求一致参数校验?

理解了什么是参数校验,咱们持续来看下个问题。现在咱们就要加参数校验了,假如是一个办法,那很简略。可是假如一个项目的所有接口办法都需求校验呢?想想就头大,会呈现许多重复且繁琐的代码,而且校验代码和事务代码混在一同,耦合太重。那么怎么处理呢?

这便是今日咱们的第二个问题,咱们需求一致参数校验。假如你看过我之前介绍的《SpringBoot怎么完成AOP》 那么就会想到,这儿咱们能够经过注解来进行切面参数校验。只需求在想要校验的办法上,加上相应的注解,办法在执行时,就会先走咱们的切面办法,对入参进行校验。这样经过一致参数校验,就处理了上面咱们说到的耦合、重复等问题。

当然,这个校验注解其实有人现已给咱们供给了,有现成好用的了。这便是今日咱们的主角Spring Validator结构和Javax Valid。


三、@Validated和@Valid差异

上面咱们说到了,参数校验现已有现成的结构了,一个是Spring Validator结构,一个是Javax Valid,那么这两个有什么差异呢?

先说Javax Valid,这个是Java核心包给咱们供给的,包名是validation-api。它遵从的是标准JSR-303标准,这个标准其实便是一个校验标准。因为是Java供给的,所以咱们运用时不需求独自引进。它供给的最常运用的注解便是@Valid。

再说Spring Validator,它是Spring供给的,底层其实是对hibernate-validator的二次封装。hibernate-validator是上面说到的标准JSR-303标准的变种,可是大多数仍是基于上述标准完成。Validator结构供给的最常运用的注解便是咱们说到的@Validated。

两者在运用上差异不少,先看运用当地:

  • @Validated:能够用在类型、办法和办法参数上。可是不能用在成员特点(字段)上
  • @Valid:能够用在办法、结构函数、办法参数和成员特点(字段)上

别的@Validated支撑分组校验,@Valid作为标准JSR-303标准,还没有支撑分组校验。可是它支撑加在字段上,所以支撑嵌套校验。这些详细差异,咱们在下面的文章都会说到。

概念都弄理解了,咱们持续下个问题,便是怎么运用它们?


四、怎么运用@Validated和@Valid等注解参数校验?

这儿咱们仍是以SpringBoot项目为例:

1、先引进jar包,上面现已说过了,Valid是Javax供给的,所以咱们不需求引进。只需求引进Spring Validator就能够。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

咱们点进去spring-boot-starter-validation包能够看到,实质上引进的仍是hibernate-validatro。

SpingBoot项目使用@Validated和@Valid参数校验

备注:这儿留意一下,Spring Boot 2.3以前的版本是默许引进了spring-boot-starter-validation的,不需求额定引进。以下是官网阐明:

SpingBoot项目使用@Validated和@Valid参数校验

2、一般来说咱们只需求对供给给前端的接口或许对外供给服务的接口进行校验即可。

3、参数校验一般分为几种状况,咱们需求分开独自说:

3.1 校验RequestParam参数

@GetMapping(value = "/aaa")
public void selectAaa(@RequestParam("usercode") @NotBlank String code){
    }

这种最简略,比方校验String类型的参数code,只需求在办法参数前加上@NotBlank注解,就能够校验入参code不为空,为空则进行报错。可是这儿留意一点,校验RequestParam参数时需求在类上添加@Validated注解。

3.2校验PathVariable参数

这个和校验RequestParam参数一样,类上加@Validated注解,详细需求校验的办法入参上,加对应的@NotBlank,@Min等注解校验即可。

3.3校验表单提交参数

前端表单提交时,恳求类型contentType是application/x-www-form-urlencoded或许multipart/form-data。这时参数也是以key1=value1&key2=value2这种参数办法带过来的,一般咱们能够运用RequestParam参数接纳,可是当入参比较多时,咱们一般会以实体类接纳。像下面这样:

public class demo{
    public void selectAaa(@Validated UserForm form){
    xxx事务代码
    }
}
@Data
@Accessors(chain = true)
public class UserForm {
    @NotBlank(message = "code不能为空")
    private String code;
    ……
}

在这儿咱们加了@Validated或许@Valid注解校验实体UserForm form入参,详细的校验在实体中经过 @NotBlank校验code不为空。

3.4校验RequestBody入参

这个也是咱们最常运用的校验办法,究竟Post恳求仍是占大多数。

@PostMapping(value = "/aaa")
public void selectAaa(@RequestBody @Validated UserForm form){
    }
@Data
@Accessors(chain = true)
public class UserForm {
@NotBlank(message = "code不能为空")
private String code;
……
}

上面咱们对入参实体UserForm加了@Validated或许@Valid注解,标明进行校验。详细的校验咱们在UserForm实体中,对字段code运用@NotBlank注解,进行非空校验。这样,经过几个注解,就能够完成对入参字段的校验。这儿留意和3.3的差异,这儿咱们加了@RequestBody注解,是从恳求body中解析json格局的参数到实体的。而3.3是是key1=value1&key2=value2这种url参数办法带过来的,从路径中解析到实体的。

为什么要区分各种状况,首要是为了下面咱们的反常处理,持续往下看。

4、加了校验后,假如入参不符合咱们的要求,就会抛出反常,所以咱们还需求进行反常处理。这儿也是运用Spring Boot的大局反常处理,进行捕获处理。详细能够参考《SpringBoot优雅的大局反常处理》,这儿只简略写下参数反常的捕获处理办法。

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(value = {MethodArgumentNotValidException.class,
            ConstraintViolationException.class, BindException.class})
    public Result<String> handleValidatedException(Exception e) {
        log.error("阻拦到参数校验未经过,反常信息:" + ExceptionUtil.stacktraceToString(e));
        //@RequestBody参数校验报错
        String errorMsg = "参数校验未经过: ";
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;
            errorMsg = errorMsg + ex.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(", "));
            // 直接校验详细参数报错
        } else if (e instanceof ConstraintViolationException) {
            ConstraintViolationException ex = (ConstraintViolationException) e;
            errorMsg = errorMsg + ex.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(", "));
            //校验目标参数报错
        } else if (e instanceof BindException) {
            BindException ex = (BindException) e;
            errorMsg = errorMsg + ex.getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(", "));
        }
        return Result.fail(errorMsg);
    }
}

能够看到,咱们上面的反常处理代码,分别捕获了MethodArgumentNotValidException.class, ConstraintViolationException.class, BindException.class三种反常。

ConstraintViolationException.class反常对应的便是校验RequestParam参数和校验PathVariable参数,这两种校验不经过,体系会抛出此反常。

BindException.class反常,对应的是校验表单提交参数,校验不经过,体系会抛出此反常。

MethodArgumentNotValidException.class反常对应的是校验RequestBody入参,校验不经过,体系会抛出此反常。

到这,咱们经过@Validated或许@Valid校验根本的入参,现已根本完成了。引进jar包,在对应类和办法上添加注解校验,处理校验不经过抛出的反常,就能够正常运行了。可是这些只是最根本的校验,下面再介绍一些特别场景的校验办法。


五、分组校验和嵌套校验

这两种校验办法也是咱们经常会遇到的,这也是@Validated和@Valid的一个差异。 @Validated支撑分组校验,@Valid支撑嵌套校验。

  1. 分组校验

比方咱们现在有一个入参UserForm,在接口A和B中都运用,那么正常咱们是这么校验参数的。

@PostMapping(value = "/aaa")
public void selectAaa(@RequestBody @Validated UserForm form){
    }
@Data
@Accessors(chain = true)
public class UserForm {
    @NotBlank(message = "code不能为空")
    private String code;
    @NotBlank(message = "name不能为空")
    private String name;
    private String sex;
    ……
}

咱们在接口selectAaa中校验了入参code和name。那么现在假如咱们还有个接口selectBbb,入参也是用UserForm接纳,可是这个接口咱们需求校验code,name和sex。那么咱们需求在sex上也加上@NotBlank注解。可是这样的话,咱们的接口selectAaa也会校验sex。这样问题就来了,同一个UserForm无法用在多个接口中接纳入参了。有没有好的处理办法呢?笨办法当然是再建一个UserForm2了。咱们这儿不评论这样合不合适,咱们的处理方案便是分组校验

分组校验首要分两步,第一步写一个分组校验类。

/**
 * Validation分组校验类,承继Default,不分组的默许校验
 */
public interface ValidGroup extends Default {
    interface demo extends ValidGroup {
        interface a extends demo {
        }
        interface b extends demo {
        }
        interface demo1 extends ValidGroup {
        }
    }
}

这个分组校验类能够选择承继或许不承继Default类,假如承继,那么不分组的默许也会校验。这个是什么意思呢?咱们持续往下看。

回到方才的接口selectAaa和selectBbb中,selectAaa不需求校验sex,而selectBbb需求校验。那么咱们就能够在sex字段上加分组校验。


@PostMapping(value = "/bbb")
public void selectBbb(@RequestBody
@Validated(value = ValidGroup.demo.a.class) UserForm form){
    }
@Data
@Accessors(chain = true)
public class UserForm {
    @NotBlank(message = "code不能为空")
    private String code;
    @NotBlank(message = "name不能为空")
    private String name;
    @NotBlank(groups = ValidGroup.demo.a.class, message = "性别不能为空")
    private String sex;
    ……
}

能够看到,咱们在NotBlank里加了group特点,在办法入参Validated上也加了value特点,都指定的同一个分组ValidGroup.demo.a.class。这样就能够起到分组校验的作用。selectAaa入参时,只校验code和name。SelectBbb入参时,因为指定了分组,所以会校验同一个分组下的sex字段。至于code和name是否校验,就由咱们的ValidGroup类是否承继Default决议,这儿承继了,则默许校验未分组的字段。那么SelectBbb校验的便是code,name和sex。这样咱们分组校验就完成了,咱们的同一个接纳实体,就能够用在不同的接口上了。

  1. 嵌套校验

咱们再来看一种场景,入参仍是运用UserForm接纳,可是UserForm里有个特点是喜好interest,而interest也是个实体。里边又有新的特点,比方interest也有code和name特点。

@Data
@Accessors(chain = true)
public class UserForm {
    @NotBlank(message = "code不能为空")
    private String code;
    @NotBlank(message = "name不能为空")
    private String name;
    @NotBlank(groups = ValidGroup.demo.a.class, message = "性别不能为空")
    private String sex;
    @NotNull
    private Interest interest;
    ……
}
@Data
@Accessors(chain = true)
public class Interest {
    @NotBlank
    private String code;
    @NotBlank
    private String name;
    ……
}

这个时分,咱们只在办法入参上运用@Validated校验UserForm,那么code,name,sex等特点校验都没问题,可是interest特点就会有问题了,它只会校验interest这个目标是否为空,而不会校验这个目标内部的特点code和name是否符合要求,即使是咱们在Interest里边的字段加上@NotBliank等注解了。比方有个interest入参只有name=篮球,没有传code,也是能够正常入参的。显然这样不符合咱们的入参要求,那么这种怎么校验呢,这儿就需求用到咱们的嵌套校验了。

嵌套校验也比较简略,在办法入参上,运用@Valid进行校验UserForm。而在UserForm中,咱们对Interest校验时,持续运用@Valid校验(这儿还记得咱们在上面讲@Valid和@Validated差异时说的吗?@Valid是支撑加在字段上的,所以这儿能运用@Valid)。而在Interest目标中,咱们持续运用@NotBlank等注解校验详细字段即可。

//嵌套校验
@PostMapping(value = "/bbb")
public void selectBbb(@RequestBody @Valid UserForm form){
    //xxx事务代码
    }
@Data
@Accessors(chain = true)
public class UserForm {
    @NotBlank(message = "code不能为空")
    private String code;
    @NotBlank(message = "name不能为空")
    private String name;
    @NotBlank(groups = ValidGroup.demo.a.class, message = "性别不能为空")
    private String sex;
    //这儿用@Valid加在字段上,标明需求校验内部特点,NotNull是校验interest目标是否存在
    @Valid
    @NotNull
    private Interest interest;
    //List格局也能够这么校验
    @Valid
    @NotNull
    private List<Interest> interest;
    ……
}
@Data
@Accessors(chain = true)
public class Interest {
    @NotBlank
    private String code;
    @NotBlank
    private String name;
    ……
}

这样,咱们就能够既校验interest目标是否传参了,又校验interest内部的code等字段是否符合标准了。 别的这儿的嵌套校验,留意可能发生数据绑定错误,需求在大局反常处理类中添加以下代码进行绑定初始化:


@InitBinder
private void activateDirectFieldAccess(DataBinder dataBinder) {
    dataBinder.initDirectFieldAccess();
}

这儿再列举一种嵌套校验的运用场景,有时办法入参是List格局,咱们也能够用@Validated合作@Valid做嵌套校验。 办法入参上运用@Valid校验,UserForm里运用@NotBlank等校验详细参数,这儿留意需求在类上加@Validated才能收效

@RestController
@Validated
public class test{
        //嵌套校验
@PostMapping(value = "/bbb")
public void selectBbb(@RequestBody @Valid List<UserForm> formList){
    //xxx事务代码
    }
}

六、Service层校验

上面咱们说到了,一般咱们需求入参校验的有两种状况,第一种,给前端供给的办法。第二种,便是对外供给的服务。因为假如是咱们自己的内部办法,肯定是自己来入参,就不需求校验了。可是供给给他人的办法,就没法保证了。第一种状况,给前端的接口咱们都会放在controller层,也便是咱们上面讲的各种运用情形。

可是总有特别状况,比方第二种,咱们直接经过service层供给对外的RPC接口服务。有时也需求在service层办法做入参校验。这儿简略提下service层需求留意的点。假如咱们一起定义了接口和完成类,那么@Validated注解需求加在接口上,而不是完成类上。假如只有完成类,那直接加在完成类上是没问题的。


七、结语

到这,咱们今日的文章就算结束了,我们跟着文章,应该能够在项目中满意大多数场景的运用了。当然,还有些配置因为篇幅问题这儿没提,我们后边能够根据自己的运用场景修正完善。

比方参数校验时,第一个参数校验失利了,就回来反常,无需悉数校验,这就需求敞开Fast形式。还有能够运用的校验注解,文章没有一一列出,我们在运用中也不需求记,只需求记住几个常用的注解就行了,在运用时现查就能够了。当然假如一些杂乱的逻辑,结构本身供给的注解无法满意事务需求,那就需求咱们写自定义注解,完成ConstraintValidator接口写自定义校验类来校验了。


感谢我们看到这,我是创作者卡兰,热心用简略易懂的小文章,共享总结自己的技能经历,欢迎我们关注、点赞和收藏。