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} //日期格式