1. 前言

数据字段一般都要遵循业务要求和数据库设计,所以后端的参数校验是必须的,应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的。

2. 数据校验的痛点

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

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

为了解决上面的痛点,将验证逻辑与相应的领域模型进行绑定是十分有必要的。为此产生了JSR 303 – Bean Validation 规范Hibernate ValidatorJSR-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<>(); @Override
public void initialize(Color constraintAnnotation) {
Colors[] value = constraintAnnotation.value();
List<String> list = Arrays.stream(value)
.map(Enum::name)
.collect(Collectors.toList());
COLOR_CONSTRAINTS.addAll(list); } @Override
public 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;
@Age
private 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;
@Age
private Integer age;
@NotEmpty(message = "成绩必填")
private List<Double> scores;
@Valid
@NotNull(message = "学校不能为空")
private School school;
}

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

如果你有其它问题可以通过felord.cn联系到我探讨。

5. 总结

通过校验框架我们可以专心于业务开发,本文对Hibernate Validator的使用和一些常见问题进行了梳理。我们可以通过Spring Boot统一异常处理来解决参数校验的异常信息的提示问题。具体可以通过关注:码农小胖哥 回复 valid获取相关DEMO

关注公众号:Felordcn 获取更多资讯

个人博客:https://felord.cn

Hibernate Validator校验参数全攻略的更多相关文章

  1. 【C#代码实战】群蚁算法理论与实践全攻略——旅行商等路径优化问题的新方法

    若干年前读研的时候,学院有一个教授,专门做群蚁算法的,很厉害,偶尔了解了一点点.感觉也是生物智能的一个体现,和遗传算法.神经网络有异曲同工之妙.只不过当时没有实际需求学习,所以没去研究.最近有一个这样 ...

  2. TestLink安装全攻略

    TestLink安装全攻略 此文章转自该链接--http://www.cnblogs.com/Tcorner/archive/2011/07/26/2117296.html 安装前准备 需要下载xam ...

  3. 用C#制作PDF文件全攻略

    用C#制作PDF文件全攻略 目  录 前    言... 3 第一部分 iText的简单应用... 4 第一章 创建一个Document 4 第一步 创建一个Document实例:... 5 第二步 ...

  4. 【转】轻松搞定FTP之FlashFxp全攻略

    转载网址:http://www.newhua.com/2008/0603/39163.shtml 轻松搞定FTP之FlashFxp全攻略 导读: FlashFXP是一款功能强大的FXP/FTP软件,融 ...

  5. fiddler Android下https抓包全攻略

    fiddler Android下https抓包全攻略 fiddler的http.https的抓包功能非常强大,可非常便捷得对包进行断点跟踪和回放,但是普通的配置对于像招商银行.支付宝.陌陌这样的APP ...

  6. Android-x86虚拟机安装配置全攻略

    转自Android-x86虚拟机安装配置全攻略 注:这里安装从简,具体请参考虚拟机Vmware安装运行安卓4.0详细教程 Android-x86虚拟机安装配置网上有很多,但是全部说明白的确不多,希望这 ...

  7. 用友U8客户端连接不上服务器全攻略

    用友U8客户端连接不上服务器全攻略 http://www.enet.com.cn2009年09月23日09:26 来自论坛 [导读]:如果网络不通,就让用户查找网络原因 检查步骤: 1.网络是否通? ...

  8. 【转】Perl Unicode全攻略

    Perl Unicode全攻略 耐心看完本文,相信你今后在unicode处理上不会再有什么问题. 本文内容适用于perl 5.8及其以上版本. perl internal form 在Perl看来, ...

  9. webBrowser中操作网页元素全攻略

    原文 webBrowser中操作网页元素全攻略 1.获取非input控件的值: webBrowser1.Document.All["控件ID"].InnerText; 或webBr ...

随机推荐

  1. python 的迭代

    字典的迭代: #创建字典 dict={'a':1,'b':2,'c':3} #key和value的迭代 for key,value in dict.items(): print(key,':',val ...

  2. 武科WUST-CTF2020“Tiki组 ”

    赛事信息 官网地址:https://ctfgame.w-ais.cn/参赛地址:https://ctfgame.w-ais.cn/起止时间:2020-03-27 18:00:00 - 2020-03- ...

  3. smtp 发送邮件实例

    发送邮件的关键点在于邮箱服务器地址是否一致 //smtp 服务器地址,咨询 smtp 提供商,例如 smtp.126.net 这种格式,端口和服务器地址是配套的,一般是 465 或者 25 SmtpC ...

  4. day46 作业

    # 班级表 create table class( cid int primary key auto_increment, caption char(16) ); # 学生表 create table ...

  5. 微信小程序中的深拷贝与浅拷贝问题

    最近在弄小程序项目的时候遇到了一个json对象复制的问题,也就是俗称的深拷贝与浅拷贝了. 一般用变量直接接收就是浅拷贝,那么如何理解浅拷贝与深拷贝的意义呢? 浅拷贝:只是将对象地址的复制,并没有开辟新 ...

  6. 重学c#系列——c# 托管和非托管资源(三)

    前言 c# 托管和非托管比较重要,因为这涉及到资源的释放. 现在只要在计算机上运行的,无论玩出什么花来,整个什么概念,逃不过输入数据修改数据输出数据(计算机本质),这里面有个数据的输入,那么我们的内存 ...

  7. bzoj1339[Baltic2008]Mafia*

    bzoj1339[Baltic2008]Mafia 题意: 匪徒准备从一个车站转移毒品到另一个车站,警方准备进行布控.对于每个车站进行布控都需要一定的代价,现在警方希望使用最小的代价控制一些车站,使得 ...

  8. JVM系列6-GC算法

    一.如何判定垃圾? 1.1.Reference Count引用计数法:引用计数count=0的对象 1.2.Root Seaching根可达法:从root开始不可达的对象 常见的可做GC roots的 ...

  9. node.js02 安装Node环境

    安装Node环境 在node.js01中我大概了解了什么是node.js,这次进入起步阶段,首先要安装下Node环境. 开始安装 查看当前Node环境的版本号 win+r输入cmd进入命令行,输入no ...

  10. 第一讲 Windows10系统下IDE-CLion的安装与配置

    01 为什么使用CLion?02 CLion安装方法03 CLion的基本使用04 课程形式及答疑说明 toc 参考链接: Window10上CLion极简配置教程 学生免费注册Pycharm专业版 ...