--- id: "20211009" date: "2021-10-09 15:42:00" title: "SpringBoot参数校验看这篇就够了" tags: ["java", "SpringBoot","exception","validation"] categories: - "java" - "spring boot学习" --- **本文所用到的全部代码见文末** ## 前言 任何一个项目都需要对接口做参数校验,最简单粗暴的校验方式就是在代码中硬编码来一个个检查参数,这种方式显然是很不优雅的,Spring 已经为我们设计了一套比较优雅的校验方式,本篇文章将进行详细说明。 ## 两个注解`@Valid`和`@Validated` 标题中的两个注解,想必是大家经常看到的,但是大部分人应该都不清楚这两个注解有什么区别(比如我),本节将解开你的疑惑。 首先来看看下这两个注解的来源: `@Valid`:`javax.validation.Valid`,来自于 javax,属于标准 JSR-380 规范 `@Validated`:`org.springframework.validation.annotation.Validated`,来自于 spring validation,属于 Spring 的 JSR-380 规范,是标准 JSR-380 的一个变种,有一些增强功能。 由此可以看出这两个注解大部分功能相似,但是存在一些区别。 **网上大部分文章说的是 JSR-303,JSR-303 是很老的标准了(Bean Validation 1.0),JSR-380 是目前最新的标准(Bean Validation 2.0),具体可在[此网站](https://beanvalidation.org/)中查看相关文档** ### 作用域区别 这两个注解的作用域定义如下: ```java //@Valid @Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE }) //@Validated @Target({TYPE, METHOD, PARAMETER}) ``` `@Valid`可作用于方法、**成员属性**、构造函数、方法参数、使用类型的任意语句中(这个是 Java8 中新增的) `@Validated`可作用于类型、方法、方法参数上 可以看到只有`@Valid`能够作用在成员属性上,也就是只能通过它能够实现嵌套验证(什么是嵌套验证见后文),单单`@Validated`不能实现。 ### 功能区别 对比两个注解的定义可以发现,@Validated 多了一个参数`Class[] value() default {}`,可以传入 Class 参数,此参数用于分组校验逻辑的实现。`@Valid`目前为止还不支持分组。 ## 校验内容 **本节只说明如何进行校验,校验异常全局处理见下一节** 当我们引入校验依赖后,可以发现有很多的校验注解可以使用,比如@NotNull,@Range,@Email 等等,我们仔细查看这些注解的来源,可以发现部分注解是来自于`javax.validation`,部分注解是来自于`org.hibernate.validator`,为啥呢 javax 是 JSR-380 的标准实现,hibernate 是这个规范的参考实现并扩充了一些功能。如果标准实现将 hibernate 中扩充的某些校验加入到标准后,hibernate 就会将自身的校验实现标记为过期(比如 6.2 版本中的@Email 注解) 具体有哪些校验注解可以[看看这篇文章](https://juejin.cn/post/6844903976270299149) ## 使用 ### 依赖引入 ```xml org.springframework.boot spring-boot-starter-validation 2.5.5 ``` 引入这个依赖会一起引入`javax.validation`和`org.hibernate.validator` 每个校验注解中都有一个 message 参数,此参数可传入国际化的 key,或者自定义的字符串,用于自定义错误提示。 ### url 参数校验 首先需要在类上加一个`@Validated`注解,否则无法对 url 参数进行校验 ```java @RestController @Validated public class TestController { } ``` 然后在对具体的方法参数进行校验,比如: ```java /** * url参数校验 */ @GetMapping("/test1") public Result test1(@NotBlank @RequestParam String param1, @Range(max = 100, min = 10) @RequestParam int param2) { return null; } ``` ### post 表单校验 首先定义入参类 TestBody.java ```java @Data public class TestBody { @NotBlank @Email(message = "请输入一个邮箱") private String param1; @NotNull private String param2; } ``` 然后在入口方法中对要校验的参数加上@Valid 或者@Validated ```java @PostMapping("/test2") public Result test2(@Validated @RequestBody TestBody body) { return null; } ``` ### 嵌套校验 何谓嵌套?就是一个对象中包含另外的对象。默认情况下是不会对潜逃对象中的属性进行校验的。比如新建一个 TestBody2 类,其中包含 TestBody ```java @Data public class TestBody2 { @NotBlank @Email(message = "请输入一个邮箱") private String param1; @NotNull private String param2; @NotNull private TestBody testBody; } ``` 上述情况在校验时只会校验 testBody 是否为 null,并不会校验其中的属性。必须要用@Valid(@Validated 不能修饰属性,所以只能用@valid)来注解 testBody 属性,才会对其属性进行校验。 入口方法如下: ```java /** * 嵌套校验 */ @PostMapping("/test3") public Result test3(@Valid @RequestBody TestBody2 testBody2) { throw new RuntimeException("我是test2"); } ``` ### 分组校验 在实际的业务需求中,可能一个入参类,会被多个接口使用,每个接口有不同的校验逻辑,这是就需要对校验逻辑进行分组,以在不同的接口中调用不同的校验组和。例子如下: 建立 TestBody3 ```java @Data public class TestBody3 { @NotBlank(groups = {Insert.class}) private String param1; @NotBlank(groups = {Update.class}) private String param2; } ``` 通过**groups**属性来说明本注解是在哪些情况下生效,如 param1 只在 Insert 模式下才会进行校验,param2 只在 Update 模式下生效,默认在入口函数中定义,如下: ```java /** * 分组校验1 */ @PostMapping("/test4") public Result test4(@Validated({Update.class}) @RequestBody TestBody3 testBody) { return null; } /** * 分组校验2 */ @PostMapping("/test5") public Result test5(@Validated({Insert.class}) @RequestBody TestBody3 testBody) { return null; } ``` **注意分组校验必须使用`@validated`注解** ### 集合类校验 集合类校验分两种 一种是对象内集合的校验,直接使用@valid 注解该属性即可,例如: ```java @Data public class TestBody5 { @Valid private List list; } ``` 使用示例见:`com.fanxb.exceptiontest.controller.TestController#test7` 另外一直直接接收集合入参,如: ```java /** * 集合校验2(对象集合) */ @PostMapping("/test8") public Result test8(@RequestBody List<@Valid TestBody4> list) { return null; } ``` 这种需要使用`@Valid`来注解泛型类,使用示例见`com.fanxb.exceptiontest.controller.TestController#test8` ### 自定义校验 最后,校验依赖提供的校验逻辑是有限的,有时我们需要进行一些特殊的校验,这就需要实现自定义校验注解,实现流程如下: 首先定义注解 ```java @Target({ElementType.FIELD}) //作用于字段 @Retention(RetentionPolicy.RUNTIME)//生命周期 @Constraint(validatedBy = CustomCheckValidator.class)//校验逻辑实现类 public @interface CustomCheck { String message() default "自定义校验默认错误提示"; /** * 自定义参数,可传递到校验实现类CustomCheckValidator中 */ String param1() default ""; Class[] groups() default {}; //用于分组校验 Class[] payload() default {}; } ``` 然后编写校验逻辑类 ```java public class CustomCheckValidator implements ConstraintValidator { private String param1; @Override public void initialize(CustomCheck constraintAnnotation) { //在此获取校验参数 param1 = constraintAnnotation.param1(); ConstraintValidator.super.initialize(constraintAnnotation); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { return param1 != null && param1.equals(s); } } ``` 最后即可向正常校验注解一样使用。详情见代码`TestController#test6` ## 校验异常处理 ### `BindingResult`处理 前一节讲了如何进行参数校验,那么参数校验的结果怎么处理呢?一种方式是将校验结果放在`BindingResult`中,如下: ```java /** * 自定义校验 */ @PostMapping("/test6") public Result test6(@Validated @RequestBody TestBody4 testBody,BindingResult result) { if(result.hasErrors()){ log.info("asdf"); } return null; } ``` 显而易见这种处理方式很繁琐,需要对每个接口都做一样的处理,不推荐使用。 **推荐使用全局异常处理来对校验结果进行统一处理** ### 全局异常处理 [上一篇:blog.fleyx.com/blog/detail/20210927/](https://blog.fleyx.com/blog/detail/20210927/)中详细说明了springboot的全局异常处理,不了解的可以去看看。 在全局异常处理类`Exceptionhandle`中能对全部的异常进行捕获处理,那么我们只需要找到参数校验抛出的异常,然后针对这些异常进行处理就可以了。代码如下: ```java @RestControllerAdvice @Slf4j public class ExceptionHandle { @ExceptionHandler(Exception.class) public Result handleException(Exception e) { BaseException be; if (e instanceof BaseException) { be = (BaseException) e; //手动抛出的异常,仅记录message log.info("baseException:{}", be.getMessage()); if (be instanceof CustomBusinessException) { //可在这里进行一些针对特定异常的处理逻辑 log.info("customBusinessException:{}", be.getMessage()); } } else if (e instanceof ConstraintViolationException) { //url参数、数组类参数校验报错类 ConstraintViolationException ce = (ConstraintViolationException) e; //针对参数校验异常,建立了一个异常类 be = new CustomValidException(ce.getMessage()); } else if (e instanceof MethodArgumentNotValidException) { //json对象类参数校验报错类 MethodArgumentNotValidException ce = (MethodArgumentNotValidException) e; be = new CustomValidException(Objects.requireNonNull(ce.getFieldError()).getDefaultMessage()); } else { //其它异常,非自动抛出的,无需给前端返回具体错误内容(用户不需要看见空指针之类的异常信息) log.error("other exception:{}", e.getMessage(), e); be = new BaseException("系统异常,请稍后重试", e); } return new Result(be.getCode(), be.getMessage(), null); } } ``` 核心是对`ConstraintViolationException`和`MethodArgumentNotValidException`两种异常的处理。 ## 结尾 本篇收集了大量的材料,对参数校验的相关内容覆盖应该比较全面了,码字不已,望点赞收藏。 **本篇用到的全部代码见:**[github](https://github.com/FleyX/demo-project/tree/master/spring-boot/paramsCheck)