SpringBoot(八) JSR-303 数据验证

快速完成参数后台数据校验,轻松搞定数据有效性验证,留出更多的时间来和小姐姐聊天…

对于任何一个应用而言,客户端做的数据有效性验证都不是安全有效的,而数据验证又是一个企业级项目架构上最为基础的功能模块,这时候就要求我们在服务端接收到数据的时候也对数据的有效性进行验证。为什么这么说呢?往往我们在编写程序的时候都会感觉后台的验证无关紧要,毕竟客户端已经做过验证了,后端没必要在浪费资源对数据进行验证了,但恰恰是这种思维最为容易被别人钻空子。毕竟只要有点开发经验的都知道,我们完全可以模拟 HTTP 请求到后台地址,模拟请求过程中发送一些涉及系统安全的数据到后台,后果可想而知….

引言

下面这段代码很多人一定见到过,就是对参数进行有效性校验,但仔细观察的话就会发现;随着参数的增加,格式的变化,校验数据有效性的代码愈发的繁琐杂乱,一点都不轻松

1
2
3
4
5
6
7
8
9
public String test1(String name) {
if (name == null) {
throw new NullPointerException("name 不能为空");
}
if (name.length() < 2 || name.length() > 10) {
throw new RuntimeException("name 长度必须在 2 - 10 之间");
}
return "success";
}

JSR-303 介绍

这里只列举了 javax.validation 包下的注解,同理在 spring-boot-starter-web 包中也存在 hibernate-validator 验证包,里面包含了一些 javax.validation 没有的注解,有兴趣的可以看看点击跳转

注解 说明
@NotNull 限制必须不为null
@NotEmpty 验证注解的元素值不为 null 且不为空(字符串长度不为0、集合大小不为0)
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在 min 到 max 之间 (支持String, Collection, Map 和数组)
@Length(max,min) 检查该字符串的长度是否在min 和 max规定的范围内 (支持String类型)
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字

构建项目

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.*;

/**
* @author Ray
* @date 2018/7/25 0025
*/
@Data
public class User {

@NotBlank(message = "name 不能为空")
@Length(min = 0, max = 10, message = "name 长度必须在 {min} - {max} 之间")
private String name;

@Min(value = 1, message = "年龄最小值为1")
@Max(value = 100, message = "年龄最大值为100")
private int age;

@NotBlank(message = "email 不能为空")
@Email(message = "email 格式不正确")
private String email;
}

控制层

@Validated 表示开启数据有效性校验,添加在类上即为验证方法,添加在方法参数中即为验证参数对象。(添加在方法上无效)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @author Ray
* @date 2018/7/25 0025
* @Validated: 开启数据有效性校验
*/
@Validated
@RestController
public class ValidateController {

/**
* 不验证
*/
@GetMapping("/test1")
public String test1(User user){
return "success";
}

/**
* 开启验证并验证对象
* 对象验证
*/
@GetMapping("/test2")
public String test2(@Validated User user){
return "success";
}

/**
* 开启验证
* 普通参数属性验证
*/
@GetMapping("/test3")
public String test3(@NotBlank(message = "name 不能为空") @Length(min = 0, max = 10, message = "name 长度必须在 {min} - {max} 之间") String name){
return "success";
}
}

验证结果

验证@NotBlank

验证@Length

自定义 Validator 注解

为何要自定义

javax.validation 包与 hibernate-validator 包中存在的注解几乎可以满足大部分的要求,又拥有基于正则表达式的@Pattern,为什么还需要自己去定义呢?

原因

(1)正则效率不高
(2)正则可读性不好
(3)正则门槛较高,很多开发者并不会编写正则表达式

自定义注解

这里定义了一个 @DateTime 注解,在该注解上标注了 @Constraint 注解,它的作用就是指定一个具体的校验器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @author Ray
* @date 2018/7/25 0025
* 自定义注解
* FIELD 约束相关的属性;PARAMETER 约束相关的参数
*/
@Target({ElementType.FIELD, ElementType.PARAMETER}) // 约束注解应用的目标元素类型
@Retention(RetentionPolicy.RUNTIME) // 约束注解应用的时机
@Constraint(validatedBy = DateTimeValidator.class) // 与约束注解关联的验证器
public @interface DateTime {

/**
* 约束注解验证时的输出消息 - 关键字段
*/
String message() default "格式错误";

/**
* 约束注解验证时的格式
*/
String format() default "yyyy-MM-dd";

/**
* 约束注解在验证时所属的组别 - 关键字段
*/
Class<?>[] groups() default {};

/**
* 约束注解的有效负载 - 关键字段
*/
Class<? extends Payload>[] payload() default {};

}

具体验证

定义校验器类 DateTimeValidator 实现 ConstraintValidator 接口,实现接口后需要实现它里面的 initializeisValid 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* @author Ray
* @date 2018/7/4 0004
* 日期格式验证
* 实现 initialize 与 isValid 方法
*/
public class DateTimeValidator implements ConstraintValidator<DateTime, String> {

private DateTime dateTime;

/**
* 主要用于初始化,它可以获得当前注解的所有属性
*/
@Override
public void initialize(DateTime dateTime) {
this.dateTime = dateTime;
}

/**
* 进行约束验证的主体方法,
* 其中 value 就是验证参数的具体实例,
* context 代表约束执行的上下文环境
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果 value 为空则不进行格式验证,为空验证可以使用 @NotBlank @NotNull @NotEmpty 等注解来进行控制,职责分离
if(value == null){
return true;
}

String format = dateTime.format();
if(value.length() != format.length()){
return false;
}

SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
try{
simpleDateFormat.parse(value);
} catch (ParseException e) {
e.printStackTrace();
return false;
}
return true;
}
}

控制层

1
2
3
4
5
6
7
8
/**
* 开启验证
* 自定义验证
*/
@GetMapping("/test4")
public String test4(@DateTime(message = "您输入的格式错误,正确的格式为:{format}", format = "yyyy-MM-dd") String date){
return "success";
}

测试结果

验证失败

验证成功

分组验证

在自定有数据有效性校验注解中介绍到注解需要有一个 groups 属性,这个属性的作用又是什么呢?

有的时候,我们对一个实体类需要有多中验证方式,在不同的情况下使用不同验证方式,比如说对于一个实体类来的 id 来说,新增的时候是不需要的,对于更新时是必须的,这个时候你是选择写一个实体类呢还是写两个呢?

创建验证组

定义一个验证组,里面写上不同的空接口类即可

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.ray.springboot208.validator;

/**
* @author Ray
* @date 2018/7/25 0025
* 验证组
*/
public class Groups {

public interface Update{};

public interface Default{};
}

实体类

groups 属性的作用就让 @Validated 注解只验证与自身 value 属性相匹配的字段,可多个,只要满足就会去纳入验证范围

我们都知道针对新增的数据我们并不需要验证 ID 是否存在,我们只在做修改操作的时候需要用到,因此这里将 ID 字段归纳到 Groups.Update.class 中去,而其它字段是不论新增还是修改都需要用到所以归纳到 Groups.Default.class中…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author Ray
* @date 2018/7/25 0025
* 简单分组
*/
@Data
public class Book {

@NotNull(message = "id 不能为空", groups = Groups.Update.class)
private Integer id;

@NotNull(message = "name 不能为空", groups = Groups.Default.class)
private String name;

@NotNull(message = "price 不能为空", groups = Groups.Default.class)
private BigDecimal price;
}

控制层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 分组验证:insert
* 不关心 id 字段
*/
@GetMapping("/insert")
public String insert(@Validated(value = {Groups.Default.class}) Book book){
return "insert";
}

/**
* 分组验证:update
* 关心 id 字段
*/
@GetMapping("/update")
public String update(@Validated(value = {Groups.Default.class, Groups.Update.class}) Book book){
return "update";
}

验证结果

分组验证 insert

验证成功

分组验证 update

验证失败

验证成功

-------------- 本文结束  感谢您的阅读 --------------