​ 在前端接收数据和前端向后端传递数据的时候,都需要进行数据校验,避免传入错误的信息,比如在需要传入一个非空的值时,传入了一个空字符串,需要传入邮箱号码的时候,传入的非邮箱格式的数据。同时在写接口时经常要写效验请求参数逻辑,这时候我们会常用做法是写大量的 if 与 if else 类似这样的代码,大量if-else代码看起来比较混乱,降低了代码的可读性。

一.JSR303数据校验

1.引入依赖

1
2
3
4
5
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.7.Final</version>
</dependency>

2.给实体类添加校验注解,并定义自己的message提示

常用的检验注解

image-20230612170704286

示例

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.atguigu.gulimall.product.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;

import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

/**
* 品牌
*
* @author JasonGong
* @email JasonGong@gmail.com
* @date 2023-05-19 00:23:36
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;

/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交") //非空的校验
private String name;
/**
* 品牌logo地址
*/
@NotEmpty//既不能为空,也要符合URL的格式
@URL(message = "logo必须是一个合法的URL地址")//URL的校验
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")//正则表达式的校验,在java中使用正则的时候不需要/符号
private String firstLetter;
/**
* 排序
*/
@NotNull //Integer类型不为空,使用@NotNull不能使用@NotEmpty
@Min(value = 0, message = "排序必须大于等于0")//最小值是0
private Integer sort;

}

3.开启校验功能@Valid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 保存
* @Valid注解开启校验
* @param result 检验之后响应的结果信息
* 给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result/*紧跟在需要校验的字段后面*/){
if(result.hasErrors()){//判断有没有校验的错误
Map<String,String> map = new HashMap<>();
//获取检验的错误结果, result.getFieldErrors():校验出错的字段的集合
for (FieldError fieldError : result.getFieldErrors()) {
String defaultMessage = fieldError.getDefaultMessage();//获取到错误的提示
String field = fieldError.getField();//获取错误的属性(字段)的名字
//封装在一个集合中方便后期统一通过R返回结果
map.put(field,defaultMessage);
}
return R.error(400,"提交的数据不合法").put("data",map);
}else {
//校验合格的话,就做进行业务的处理
brandService.save(brand);
}
return R.ok();
}

测试查看返回的数据的格式,这里我们输入的都是不合法的数据格式,返回的结果如下

1
2
3
4
5
6
7
8
9
10
{
"msg": "提交的数据不合法",
"code": 400,
"data": {
"name": "品牌名必须提交",//字段:错误信息
"logo": "logo必须是一个合法的URL地址",
"sort": "排序必须大于等于0",
"firstLetter": "检索首字母必须是一个字母"
}
}

参数没有错误之后返回的数据

1
2
3
4
{
"msg": "success",
"code": 0
}

4.上面的代码过于冗余,我们可以直接使用统一异常处理处理数据校验的异常

在统一异常处理类上加上数据校验异常的异常处理

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
package com.atguigu.gulimall.product.exception;

import com.atguigu.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/6/12
* @Description 异常处理类
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e){
log.error("数据校验出现问题:{},异常类型:{}",e.getMessage(),e.getClass());
Map<String,String> map = new HashMap<>();
//获取检验的错误结果
BindingResult result = e.getBindingResult();
for (FieldError fieldError : result.getFieldErrors()) {
//获取到错误的提示
String defaultMessage = fieldError.getDefaultMessage();
//获取错误的属性的名字
String field = fieldError.getField();
map.put(field,defaultMessage);
}
return R.error(400,"数据校验出现问题").put("data",map);
}
}

这个时候上面的代码就可以简化为下面的格式,数据校验出现问题之后就直接在统一异常处理中处理了

1
2
3
4
5
6
7
8
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand ){
brandService.save(brand);
return R.ok();
}

5.分组校验功能

 例如:当我们在添加一个品牌的时候,我们不需要传入这个品牌的id信息,需要这个品牌的品牌名信息,但是在修改这个品牌的时候,我们需要这个品牌的id信息和品牌名的信息,这时我们就需要使用分组校验了

5.1 定义空接口,作为分组校验的组

添加操作的组

1
2
3
4
5
package com.atguigu.common.valid;

public interface AddGroup {
}

修改操作的组

1
2
3
4
5
package com.atguigu.common.valid;

public interface UpdateGroup {
}

5.2 在实体类上添加上分组的信息

注意:使用了分组校验之后,其余的字段也要加上分组信息,否则没有加上分组信息的会失效

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
package com.atguigu.gulimall.product.entity;

import com.atguigu.common.valid.AddGroup;
import com.atguigu.common.valid.UpdateGroup;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;

import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.*;

@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;

/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id", groups = {UpdateGroup.class})//在修改的时候进行校验
@Null(message = "新增不能指定id", groups = {AddGroup.class})//在添加的时候进行校验
@TableId
private Long brandId;


/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交", groups = {AddGroup.class, UpdateGroup.class})//这个就是在添加和修改的组中都需要进行校验
private String name;
}

5.3 在控制层添加上相应的注解信息,指定当前是操作属于的分组

注意:这里如果产生数据校验的出现问题的异常,会由统一异常处理进行处理

保存的控制器方法上添加上添加分组信息

1
2
3
4
5
6
7
8
9
/**
* 保存
* @Validated({AddGroup.class}) 指定使用添加分组的校验
*/
@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand ){
brandService.save(brand);
return R.ok();
}

修改的分组上添加上修改的分组信息

1
2
3
4
5
6
7
8
9
/**
* 修改
*/
@RequestMapping("/update")
public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand){
brandService.updateById(brand);

return R.ok();
}

测试:我们在新增的时候加上品牌的id,这时就会产生错误

image-20230614105450858

修改的时候不带品牌的id信息

image-20230614111024292

6.自定义校验

这里我们以编写一个输入的值只能是指定值的注解为例

1
2
3
4
5
6
 /**
* 显示状态[0-不显示;1-显示]
*/
//注意:我们使用了分组校验,所以这里也要加上分组groups
@ListValue(vals = {0, 1}, groups = {AddGroup.class, UpdateGroup.class})//自定义的注解,输入的时候只能是0或者1
private Integer showStatus;

6.1 编写一个自定义的校验注解

编写自定义注解

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
package com.atguigu.common.valid;

import javax.validation.Constraint;
import javax.validation.Payload;

import java.lang.annotation.Documented;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/6/15
* @Description 自定义的校验注解
*/
@Documented
@Constraint(validatedBy = {})//校验注解使用的校验器
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})//注解可以标注的位置
@Retention(RUNTIME)
public @interface ListValue {

String message() default "{com.atguigu.common.valid.ListValue.message}";//在配置文件中指明

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

int[] vals() default {};//使用注解的时候会有vals属性 在属性里面指定值
}

编写注解中默认提示消息的配置文件

ValidationMessages.properties

1
com.atguigu.common.valid.ListValue.message=必须提交指定的值

6.2 编写一个自定义的校验器

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
package com.atguigu.common.valid;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/6/15
* @Description 自定义的校验器
*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();//创建一个set集合 用于存储指定输入的值0和1
//初始化方法
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();//@ListValue(vals = {0, 1})
for (int val : vals) {
set.add(val);
}
}

//判断是否校验成功
//value:需要校验的值
@Override
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
return set.contains(integer);//判断真实树的值在不在包含0和1的这个set集合中
}
}

6.3 关联自定义的校验器和校验注解

在自定义的注解上面关联上上面自定义的校验规则

image-20230615151144311

测试自定义的注解

image-20230615152331597