1.技术背景
Java API 的规范 JSR303 定义了校验的标准 validation-api ,其中一个比较出名的实现是 hibernate validation。软件技术的载体就是各种数据的传递级数据的运算,比如我们公司所开发的项目皆为ERP管理系统项目,在管理系统中,因为数据校验的不准确跟漏校验,会导致部分数据不正常显示,也可能对后续的其他操作带来一定的影响,所以数据的准确性及为重要。由此我们在软件开发过程中对数据的处理必须非常谨慎,以确保数据的准确性。数据准确性的保证无非就是两个层面:1.确保源数据的准确性、2.确保系统内部数据的准确性。今天主要讲解如何确保源数据准确性。
但是这个有个弊端参数校验的代码业务逻辑代码耦合度高,维护修改起来很不方便。通过学习发现了对其二次封装的spring validation,常用于 SpringMVC 的参数自动校验,参数校验的代码就不需要再与业务逻辑代码进行耦合了。
2.主要技术 Hibernate Validator
2.1 简介
Hibernate Validator 是 Hibernate 团队最初的数据校验框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现
JSR(Java Specification Requests) 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便!
2.2 常用注解
@Null 限制只能为null
@NotNull 限制必须不为null
@AssertFalse 限制必须为false
@AssertTrue 限制必须为true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Future 限制必须是一个将来的日期
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@Past 限制必须是一个过去的日期
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在min到max之间
@Past 验证注解的元素值(日期类型)比当前时间早
@NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty, @NotBlank只应用于字符串且在比较时会去除字符串的空格
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
3.业务场景
主要校验数据项较多的表单,更多涉及的是一些新增入库操作,和数据更新操作,一般查询表单数据可根据业务需求进行灵活配置。
4.使用样例
4.1 相关依赖引入
- maven依赖 非web场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
- springboot的web开发场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
4.2 实际应用
@PathVariable 和 @RequestParam 参数校验
Get 请求的参数接收一般依赖这两个注解,但是处于 url 有长度限制和代码的可维护性,超过 5 个参数尽量用实体来传参。对 @PathVariable 和 @RequestParam 参数进行校验需要在入参声明约束的注解。如果校验失败,会抛出 MethodArgumentNotValidException 异常。单个参数校验如下
@PostMapping("/testOneDataNotNull")
public Result testOneDataNotNull(@RequestParam("data") @NotNull(message = "数据不能为空!") String data) {
return Result.success("NotNull单个数据验证通过!", data);
}
2、@RequestBody 参数校验
Post、Put 请求的参数推荐使用 @RequestBody 请求体参数。对 @RequestBody 参数进行校验需要在 DTO 对象中加入校验条件后,再搭配 @Validated 即可完成自动校验。如果校验失败,会抛出 ConstraintViolationException 异常。
@PostMapping("/user")
public Result formDataTest(@RequestBody @Valid User user) {
return Result.success("表单数据合法!", user);
}
public class User {
@Min(value = 1, message = "id值大于0")
private Integer id;
@NotBlank(message = "用户名称不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
@Size(max = 16, message = "联系方式长度不能大于16")
private String contact;
@Email(message = "邮箱格式不正确")
@Size(max = 256, message = "邮箱长度不能大于256")
private String email;
}
分组校验
业务场景:比如新增一个用户时,数据库的uuid字段一般为自动生成的,表单中不需要校验id字段,但更新和删除时,我们就需要验证uuid是否为空。
5.其他拓展
5.1 统一异常捕获
// RequestBody 上validate失败后抛出的异常是MethodArgumentNotValidException异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException exception){
Result result =new Result();
String message = exception.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
log.info("MethodArgumentNotValidException-----requestBody参数映射失败");
log.info(exception.getMessage());
result.setCode(400);
result.setMessage(message);
return result;
}
//Valid 验证路径中请求实体校验失败后抛出的异常
@ExceptionHandler(BindException.class)
public Result BindExceptionHandler(BindException exception){
Result result = new Result();
String message = exception.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
log.info("BindException------参数校验错误");
log.info(message);
result.setCode(400);
result.setMessage(message);
return result;
}
5.2 扩展内容 @Pattern(value)中常用正则表达式
- 元字符
字符 | 描述 |
\ | 将下一个字符标记为一个特殊字符、或一个原义字符、或一个向后引用、或一个八进制转义符。例如,“n”匹配字符“n”。“\n”匹配一个换行符。序列“\”匹配“\”而“(”则匹配“(”。 |
^ | 匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,^也匹配“\n”或“\r”之后的位置。 |
$ | 匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。 |
* | 匹配前面的子表达式零次或多次。例如,zo能匹配“z”以及“zoo”。等价于{0,}。 |
+ | 匹配前面的子表达式一次或多次。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。 |
? | 匹配前面的子表达式零次或一次。例如,“do(es)?”可以匹配“do”或“does”中的“do”。?等价于{0,1}。 |
\d | 匹配一个数字字符。等价于 [0-9]。 |
\D | 匹配一个非数字字符。等价于 [^0-9]。 |
\w | 匹配字母、数字、下划线。等价于'[A-Za-z0-9_]'。 |
\W | 匹配非字母、数字、下划线。等价于'[^A-Za-z0-9_]'。 |
{n} | n是一个非负整数。匹配确定的n*次。例如,“o{2}”不能匹配“Bob”中的“o”,但是能匹配“food”中的两个o。 |
{n,} | n 是一个非负整数。至少匹配n 次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'。 |
{n,m} | m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,"o{1,3}" 将匹配 "fooooood" 中的前三个 o。'o{0,1}' 等价于 'o?'。请注意在逗号和两个数之间不能有空格。 |
x|y | 匹配 x 或 y。例如,'z |
[xyz] | 字符集合。匹配所包含的任意一个字符。例如, '[abc]' 可以匹配 "plain" 中的 'a'。 |
[^xyz] | 负值字符集合。匹配未包含的任意字符。例如,“^abc”可以匹配“plain”中的“p”。 |
[a-z] | 字符范围。匹配指定范围内的任意字符。例如,'[a-z]' 可以匹配 'a' 到 'z' 范围内的任意小写字母字符。 |
[^a-z] | 负值字符范围。匹配任何不在指定范围内的任意字符。例如,'^a-z' 可以匹配任何不在 'a' 到 'z' 范围内的任意字符。 |
- 常用正则表达式
^[a-z0-9_-]{3,16}$ //用户名
^[a-z0-9_-]{6,18}$ //密码
^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$ //电子邮箱
^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$ //URL
^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$ //IP地址
\d{3}-\d{8}|\d{4}-\d{7} //电话号码
[1-9][0-9]{4,} //QQ号
[1-9]\d{5}(?!\d) //邮政编码
\d{15}|\d{18} //身份证
^[1-9]\d*$ //匹配正整数
^-[1-9]\d*$ //匹配负整数
^-?[1-9]\d*$ //匹配整数
^[1-9]\d*|0$ //匹配非负整数(正整数 + 0)
^\d{4}-\d{1,2}-\d{1,2} //日期格式