1. 前言

因为网络传输的不可靠性,以及前端数据控制的可篡改性,后端的参数校验是必须的,应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的。

2. 数据校验的痛点

为了保证数据语义的正确,我们需要进行大量的判断来处理验证逻辑。而且项目的分层也会造成一些重复的校验,产生大量与业务无关的代码。不利于代码的维护,增加了开发人员的工作量。

3. JSR 303 校验规范及其实现

为了解决上面的痛点,将验证逻辑与相应的领域模型进行绑定是十分有必要的。为此产生了JSR 303 – Bean Validation 规范。Hibernate Validator 是JSR-303的参考实现,它提供了JSR 303规范中所有的约束(constraint)的实现,同时也增加了一些扩展。

Hibernate Validator 提供的常用约束注解

约束注解 详细信息
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式
@Email 被注释的元素必须是电子邮箱地址
@Length 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range 被注释的元素必须在合适的范围内

4. 验证注解的使用

Spring Boot开发中使用Hibernate Validator是非常容易的,引入下面的starter就可以了:

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

一种可以实现接口来定制Validator,一种是使用约束注解。胖哥觉得注解可以满足绝大部分的需求,所以建议使用注解来进行数据校验。而且注解更加灵活,控制的粒度也更加细。接下来我们来学习如何使用注解进行数据校验。

4.1 约束注解的基本使用

我们对需要校验的方法入参进行注解约束标记,例子如下:

@Data
public class Student {@NotBlank(message = "姓名必须填")private String name;@NotNull(message = "年龄必须填写")@Range(min = 1,max =50, message = "年龄取值范围1-50")private Integer age;@NotEmpty(message = "成绩必填")private List<Double> scores;
}

POST 请求

然后定义一个POST请求的Spring MVC接口:

@RestController
@RequestMapping("/student")
public class StudentController {@PostMapping("/add")public Rest<?> addStudent(@Valid @RequestBody Student student) {return RestBody.okData(student);}
}

通过对addStudent方法入参添加@Valid来启用参数校验。当使用下面数据进行请求将会抛出MethodArgumentNotValidException异常,提示age范围超出1-50

POST /student/add HTTP/1.1
Host: localhost:8888
Content-Type: application/json{"name": "felord.cn","age": 77,"scores": [55]
}

GET 请求

如法炮制,我们定义一个GET请求的接口:

@GetMapping("/get")
public Rest<?> getStudent(@Valid Student student) {return RestBody.okData(student);
}

使用下面的请求可以正确对学生分数scores进行了校验,但是抛出的并不是MethodArgumentNotValidException异常,而是BindException异常。这和使用@RequestBody注解有关系,这对我们后面的统一处理非常十分重要。

GET /student/get?name=felord.cn&age=12 HTTP/1.1
Host: localhost:8888

自定义注解

可能有些同学注意到上面的年龄我进行了这样的标记:

@NotNull(message = "年龄必须填写")
@Range(min = 1,max =50, message = "年龄取值范围1-50")
private Integer age;

这是因为@Range不会去校验为空的情况,它只处理非空的时候是否符合范围约束。所以要用多个注解来约束。如果我们某些场景需要重复的捆绑多个注解来使用时,可以使用自定义注解将它们封装起来组合使用,下面这个注解就是将@NotNull@Range进行了组合,你可以仿一个出来用用看。

import org.hibernate.validator.constraints.Range;import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotNull;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import java.lang.annotation.*;/*** @author a* @since 17:31**/
@Constraint(validatedBy = {}
)
@SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD,ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,ElementType.PARAMETER, ElementType.TYPE_USE})
@NotNull
@Range(min = 1, max = 50)
@Documented
@ReportAsSingleViolation
public @interface Age {// message 必须有String message() default "年龄必须填写,且范围为 1-50 ";// 可选Class<?>[] groups() default {};// 可选Class<? extends Payload>[] payload() default {};
}

还有一种情况,我们在后台定义了枚举值来进行状态的流转,也是需要校验的,比如我们定义了颜色枚举:

public enum Colors {RED, YELLOW, BLUE}

我们希望入参不能超出Colors的范围["RED", "YELLOW", "BLUE"],这就需要实现ConstraintValidator<A extends Annotation, T>接口来定义一个颜色约束了,其中泛型A为自定义的约束注解,泛型T为入参的类型,这里使用字符串,然后我们的实现如下:

/*** @author felord.cn* @since 17:57**/
public class ColorConstraintValidator implements ConstraintValidator<Color, String> {private static final Set<String> COLOR_CONSTRAINTS = new HashSet<>();@Overridepublic void initialize(Color constraintAnnotation) {Colors[] value = constraintAnnotation.value();List<String> list = Arrays.stream(value).map(Enum::name).collect(Collectors.toList());COLOR_CONSTRAINTS.addAll(list);}@Overridepublic boolean isValid(String value, ConstraintValidatorContext context) {return COLOR_CONSTRAINTS.contains(value);}
}

然后声明对应的约束注解Color,需要在元注解@Constraint中指明使用上面定义好的处理类ColorConstraintValidator进行校验。

/*** @author felord.cn* @since 17:55**/
@Constraint(validatedBy = ColorConstraintValidator.class)
@Documented
@Target({ElementType.METHOD, ElementType.FIELD,ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Color {// 错误提示信息String message() default "颜色不符合规格";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};// 约束的类型Colors[] value();
}

然后我们来试一下,先对参数进行约束:

@Data
public class Param {@Color({Colors.BLUE,Colors.YELLOW})private String color;
}

接口跟上面几个一样,调用下面的接口将抛出BindException异常:

GET /student/color?color=CAY HTTP/1.1
Host: localhost:8888

当我们把参数color赋值为BLUE或者YELLOW后,能够成功得到响应。

4.2 常见问题

在实际使用起来我们会遇到一些问题,这里总结了一些常见的问题和处理方式。

检验基础类型不生效的问题

上面为了校验颜色我们声明了一个Param对象来包装唯一的字符串参数color,为什么直接使用下面的方式定义呢?

@GetMapping("/color")
public Rest<?> color(@Valid @Color({Colors.BLUE,Colors.YELLOW}) String color) {return RestBody.okData(color);
}

或者使用路径变量:

@GetMapping("/rest/{color}")
public Rest<?> rest(@Valid @Color({Colors.BLUE, Colors.YELLOW}) @PathVariable String color) {return RestBody.okData(color);
}

上面两种方式是不会生效的。不信你可以试一试,起码在Spring Boot 2.3.1.RELEASE是不会直接生效的。

使以上两种生效的方法是在类上添加@Validated注解。注意一定要添加到方法所在的类上才行。这时候会抛出ConstraintViolationException异常。

集合类型参数中的元素不生效的问题

就像下面的写法,方法的参数为集合时,如何检验元素的约束呢?

/*** 集合类型参数元素.** @param student the student* @return the rest*/
@PostMapping("/batchadd")
public Rest<?> batchAddStudent(@Valid @RequestBody List<Student> student) {return RestBody.okData(student);
}

同样是在类上添加@Validated注解。注意一定要添加到方法所在的类上才行。这时候会抛出ConstraintViolationException异常。

嵌套校验不生效

嵌套的结构如何校验呢?打个比方,如果我们在学生类Student中添加了其所属的学校信息School并希望对School的属性进行校验。

@Data
public class Student {@NotBlank(message = "姓名必须填")private String name;@Ageprivate Integer age;@NotEmpty(message = "成绩必填")private List<Double> scores;@NotNull(message = "学校不能为空")private School school;
}@Data
public class School {@NotBlank(message = "学校名称不能为空")private String name;@Min(value = 0,message ="校龄大于0" )private Integer age;
}

GET请求时正常校验了School的属性,但是POST请求却无法对School的属性进行校验。这时我们只需要在该属性上加上@Valid注解即可。

@Data
public class Student {@NotBlank(message = "姓名必须填")private String name;@Ageprivate Integer age;@NotEmpty(message = "成绩必填")private List<Double> scores;@Valid@NotNull(message = "学校不能为空")private School school;
}

每加一层嵌套都需要加一层@Valid注解。通常在校验对象属性时,@NotNull@NotEmpty@Valid配合才能起到校验效果。

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

请不要再使用判断进行参数校验了相关推荐

  1. 【全网最全】JSR303参数校验与全局异常处理(从理论到实践别用if判断参数了)

    一.前言 我们在日常开发中,避不开的就是参数校验,有人说前端不是会在表单中进行校验的吗?在后端中,我们可以直接不管前端怎么样判断过滤,我们后端都需要进行再次判断, 为了安全 .因为前端很容易拜托,当测 ...

  2. Spring基础系列-参数校验

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9953744.html Spring中使用参数校验 概述 ​ JSR 303中提出了Bea ...

  3. 自定义检验注解_多注解自定义参数校验

    好久直接更新了单个注解参数校验以后.偷懒了好久,今天就把多个注解自定义的参数校验写了一下,思路是一样的,只是一个注解包含的字段把它分解成了多个注解,类似于java-valid自带的不同功能的校验注解一 ...

  4. SpringBoot全局异常处理及前端请求参数校验

    SpringBoot全局异常捕获处理及参数校验 文章目录 SpringBoot全局异常捕获处理及参数校验 为什么要用全局异常处理? 如何进行全局异常捕获和处理? 统一结果封装 统一返回结果 枚举类 使 ...

  5. Springboot @Validated参数校验

    简单使用 Java API规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现.hibernate validation是对这个规范的实现,并增加了校验注解如@Em ...

  6. Spring Validation最佳实践及其实现原理,参数校验没那么简单!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:六点半起床 juejin.im/post/685654110 ...

  7. SpringBoot实现通用的接口参数校验

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 作者:cipher juejin.im/post/5af3c25b ...

  8. 二胖写参数校验的坎坷之路

    背景 最近端午好久没有和二胖聚一聚了,于是约了二胖到人民广场去宰他一顿,正好最近他跳槽加薪了. 我:二胖听说你最近跳槽了,并且还是从传统软件公司跳到了互联网公司,工资是不是涨了一点啊,今天你请客哈. ...

  9. java 接口参数验证_SpringBoot实现通用的接口参数校验

    作者:cipher 来源:http://39sd.cn/560BA 本文介绍基于Spring Boot和JDK8编写一个AOP,结合自定义注解实现通用的接口参数校验. 缘由 目前参数校验常用的方法是在 ...

最新文章

  1. 记录在Ubuntu14.04上安装ryu中遇到的各种坑
  2. App开发流程之右滑返回手势功能续
  3. c语言 getch头文件,用getch()需要头文件吗?
  4. linux数据库服务器的配置与管理,实验七_Linux_MySQL数据库服务器配置与管理
  5. thinkphp5(改变html中的变量、赋值和替换)
  6. SQL基础【七、Order by】
  7. oracle中的cursor属性有哪些,Cursor语法及理解
  8. http.ListenAndServe()到底做了什么?
  9. Android 软键盘相关问题
  10. mysql 从库可以写入吗_mysql主从库配置读写分离以及备份
  11. 知识点整理-mysql怎么查看优化器优化后的sql
  12. Java——类的继承
  13. python 在Excel中新增一列
  14. 190308每日一句
  15. 倒立摆控制系统matlab,单轴倒立摆控制系统设计及Matlab仿真毕业设计论文(资料4)...
  16. 数据中心空调系统基础知识-制冷量与热量计算
  17. 五指山(nefu 84)
  18. 【LeetCode】【VSCode】在VSCode中使用插件刷题
  19. C PRIMER PLUS第七章11题
  20. Feign中启用Hystrix实现容错处理

热门文章

  1. taylor+swift纽约公寓_国际巨星Taylor Swift有多爱豪宅?才30岁就有8套豪宅
  2. python滚动条自动向下,python tqdm 如何使得滚动条不上下滚动(保持一行内滚动)
  3. HashMap 源码
  4. python编辑程序模型_python并发编程之IO模型
  5. python怎么暂停爬虫_python Python爬虫防封杀方法集合
  6. Windows自带渗透工具Certutil介绍(免杀、哈希计算、md5、sha256、下载文件、base64编码)
  7. Ubuntu开机启动Python脚本
  8. Redis 主从复制的几种方法
  9. windows10环境下载labelImg及使用方法
  10. Luogu P2319 [HNOI2006]超级英雄