参数校验(一):参数校验的介绍和简单使用

SpringBoot参数校验的使用

参数校验分为:简单校验、嵌套校验、分组校验

简单校验

  • DTO

简单校验就是在DTO类中需要校验的属性上标注注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Data
public class GoodsSearchByPageDTO {
@Pattern(regexp = "^[a-zA-Z0-9\\u4e00-\\u9fa5]{1,50}", message = "keyword内容不正确")
private String keyword;

@Pattern(regexp = "^[a-zA-Z0-9]{6,20}", message = "code内容不正确")
private String code;

@Pattern(regexp = "^职场白领$|^个人高端$", message = "type内容不正确")
private String type;

@Range(min = 1, max = 3, message = "partId范围内容不正确")
private Byte partId;

private Boolean status;

@NotNull(message = "pageCurr内容不能为空")
@Min(value = 1, message = "pageCurr不能小于1")
private Integer pageCurr;

@NotNull(message = "pageSize不能为空")
@Range(min = 10, max = 50, message = "pageSize不能小于1或大于50")
private Integer pageSize;
}

message:表示不满足时返回的错误信息。

  • Controller

以上约束标记完成之后,要想完成校验,需要在controller层的接口标注@Valid注解即可。如下所示:

1
2
3
4
5
6
7
8
9
10
@RestController("MisGoodsController")
@RequestMapping("/mis/goods")
public class GoodsController {

@PostMapping("/searchByPage")
public R searchByPage(@RequestBody @Valid GoodsSearchByPageDTO dto) {
// ...逻辑代码
return R.okPage(pageUtils);
}
}

@ValidRequestBody缺一不可。

嵌套校验

在 Spring Boot 中,我们可以使用注解实现嵌套校验。嵌套校验主要是针对复杂的业务场景,例如一个对象中包含了另一个对象,或者一个集合中包含了多个元素等情况。

嵌套校验很简单,只需要在嵌套的实体属性标注@Valid注解,则其中的属性也将会得到校验,否则不会校验。

  • DTO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class InsertAndUpdateGoodsDTO {

@NotBlank(message = "code不能为空")
@Pattern(regexp = "^[a-zA-Z0-9]{6,20}", message = "code内容不正确")
private String code;

/**
* @Valid这个注解标注在集合上,将会针对集合中每个元素进行校验
*/
@Valid
@NotEmpty(message = "列表不能为空")
private ArrayList<CheckupVO> checkup_1;
}
  • Controller
1
2
3
4
5
6
7
8
9
10
@RestController("MisGoodsController")
@RequestMapping("/mis/goods")
public class GoodsController {

@PostMapping("/insert")
public R insert(@RequestBody @Valid InsertAndUpdateGoodsForm form) {
// 业务代码
return R.okRows(rows);
}
}

总结:嵌套校验只需要在需要校验的类或集合上添加@Valid注解就可以了,在需要进行校验的接口层参数中添加 @Valid或者@Validate就可以了。

分组校验

在开发的过程中,对于不同业务,但是需要校验的数据几乎差不多,那我们就可以考虑使用分组校验,来进行一定程度的区分。\

我们创建分组接口:

如果我们不继承Default,同时我们在Web层并没有指定分组的标识,那么参数校验就会失效,可以自行尝试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface CustomValidateGroup extends Default {

interface Crud extends CustomValidateGroup {
interface Create extends Crud {

}

interface Update extends Crud {

}

interface Query extends Crud {

}

interface Delete extends Crud {

}
}
}

DTO中添加校验注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class InsertAndUpdateGoodsForm {
// 在更新的时候用不到这个
@NotNull(message = "id不能为空", groups = CustomValidateGroup.Crud.Update.class)
@Min(value = 1, message = "id值不能小于1", groups = CustomValidateGroup.Crud.Update.class)
private Integer id;

@NotBlank(message = "code不能为空",groups = CustomValidateGroup.Crud.Insert.class)
@Pattern(regexp = "^[a-zA-Z0-9]{6,20}", message = "code内容不正确",groups = CustomValidateGroup.Crud.Insert.class)
private String code;

@NotBlank(message = "title不能为空")
@Pattern(regexp = "^[a-zA-Z0-9\\u4e00-\\u9fa5]{2,50}", message = "title内容不正确")
private String title;
}

Web

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController("MisGoodsController")
@RequestMapping("/mis/goods")
public class GoodsController {

@PostMapping("/insert")
public R insert(@RequestBody @Validated({CustomValidateGroup.Crud.Insert.class} InsertAndUpdateGoodsForm form) {
// 业务代码
return R.okRows(rows);
}

@PostMapping("/update")
public R update(@RequestBody @Validated({CustomValidateGroup.Crud.Update.class}) InsertAndUpdateGoodsForm form) {
// 业务代码
return R.okRows(rows);
}
}

异常处理

既然是校验参数,有校验通过自然也就有不通过,如果参数校验失败,框架会自动抛出异常。抛出的异常MethodArgumentNotValidExceptionConstraintViolationException或者BindException异常。

因此我们可以在全局的异常处理器中捕捉到这三种异常,将参数校验失败的信息或者自定义信息返回给前端。

全局异常处理类

全局异常处理:

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
@Slf4j
@RestControllerAdvice
@ResponseBody
public class ExceptionAdvice {
/**
* BindException异常处理
* <p>
* 当BindingResult中存在错误信息时,会抛出BindException异常。
*/
@ExceptionHandler({BindException.class})
public Result<Object> handleBindExceptionException(BindException ex) {
BindingResult bindingResult = ex.getBindingResult();
return Result.fail(bindingResult.getAllErrors().stream().map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(";")));
}

/**
* MethodArgumentNotValidException异常处理
* <p>
* MethodArgumentNotValidException异常校验 @RequestBody标注的JSON对象参数
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
public Result<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
return Result.fail(bindingResult.getAllErrors().stream().map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(";")));
}

/**
* ConstraintViolationException异常处理
* <p>
* 单个参数异常处理
*/
@ExceptionHandler({ConstraintViolationException.class})
public Result<Object> handleConstraintViolationException(ConstraintViolationException ex) {
return Result.fail(ex.getConstraintViolations().stream().map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";")));
}

/**
* 兜底异常处理
*/
@ExceptionHandler({Throwable.class})
public Result<Object> handleConstraintViolationException(Throwable throwable) {
return Result.fail(throwable.getMessage());
}
}

公共响应类

公共响应结果类如下:

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
66
67
68
69
70
71
72
73
74
75
public class Result<T> {
private String code;

private String msg;

private Boolean successFlag;

private T data;

public static <T> Result<T> success() {
Result<T> result = new Result<>();
result.successFlag(true);
return result;
}

public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.successFlag(true).data(data);
return result;
}

public static <T> Result<T> fail(String errorMsg) {
Result<T> result = new Result<>();
result.successFlag(false).msg(errorMsg);
return result;
}

public static <T> Result<T> fail(String code, String errorMsg) {
Result<T> result = new Result<>();
result.successFlag(false).code(code).msg(errorMsg);
return result;
}

public static <T> Result<T> fail(ExceptionEnum exceptionEnum) {
Result<T> result = new Result<>();
result.successFlag(false).code(exceptionEnum.getCode()).msg(exceptionEnum.getErrorMsg());
return result;
}

public Result<T> code(String code) {
this.code = code;
return this;
}

public Result<T> msg(String msg) {
this.msg = msg;
return this;
}

public Result<T> successFlag(Boolean successFlag) {
this.successFlag = successFlag;
return this;
}

public Result<T> data(T data) {
this.data = data;
return this;
}

public String getCode() {
return code;
}

public String getMsg() {
return msg;
}

public T getData() {
return data;
}

public Boolean getSuccessFlag() {
return successFlag;
}
}

异常枚举类

异常枚举类如下:

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
66
67
68
69
70
public enum ExceptionEnum {
/**
* 请求错误!
*/
BAD_REQUEST("400", "请求错误!"),
/**
* 未经授权的请求!
*/
UNAUTHORIZED("401", "未经授权的请求!"),
/**
* 没有访问权限!
*/
FORBIDDEN("403", "没有访问权限!"),
/**
* 请求的资源未不到!
*/
NOT_FOUND("404", "请求的资源未不到!"),
/**
* 服务器内部错误!
*/
INTERNAL_SERVER_ERROR("500", "服务器内部错误!"),
/**
* 服务器正忙,请稍后再试!
*/
BAD_GATEWAY("502", "服务器正忙,请稍后再试!"),
/**
* 服务器正忙,请稍后再试!
*/
SERVICE_UNAVAILABLE("503", "服务器正忙,请稍后再试!"),
/**
* 网关超时!
*/
GATEWAY_TIMEOUT("504", "网关超时!"),
/**
* 非法参数异常!
*/
ILLEGAL_ARGUMENT_ERROR("10000", "非法参数异常!"),
/**
* 用户ID不能为空!
*/
USER_ID_NOT_BLANK("10001", "用户ID不能为空!"),

/**
*
*/
UNKNOWN("9999", "未知异常!");

/**
* 错误码
*/
private final String code;

/**
* 错误描述
*/
private final String errorMsg;

ExceptionEnum(String code, String errorMsg) {
this.code = code;
this.errorMsg = errorMsg;
}

public String getCode() {
return code;
}

public String getErrorMsg() {
return errorMsg;
}
}

快速失败

这样返回的信息是校验了多个错误的信息通过;进行拆分。如果我们不想要这样的话可以自行查看进行截取。

示例:

返回的错误信息msg": "电话号码不能为空;头像不能为空;身份证号不能为空;用户姓名不能为空;爱好不能为空"

当我们想要快速返回错误信息,不想要进行全部参数校验完成之后才进行返回

快速失败需要的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class ParameterValidationConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 开启 fail fast 机制
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
  • 同时我们修改一下全局异常处理的返回信息处理,拿其中两个进行举例:

我们也可以调试去查看ex类的信息进行自我选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ExceptionHandler({ConstraintViolationException.class})
public Result<Object> handleConstraintViolationException(ConstraintViolationException ex) {
ConstraintViolationException exception = (ConstraintViolationException) e;
String m = null;
for (ConstraintViolation<?> constraintViolation : exception.getConstraintViolations()) {
m = constraintViolation.getMessage();
}
return Result.fail(m);
}
@ExceptionHandler({MethodArgumentNotValidException.class})
public Result<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
return Result.fail(exception.getBindingResult().getFieldError().getDefaultMessage());
}

自定义注解校验处理

自定义注解

在Spring Boot中,可以通过自定义注解,并结合@Valid@Validated注解来实现自定义校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = MyValidator.class)// 指定校验器
public @interface MyConstraint {

String message() default "自定义校验错误信息";

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

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

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {

MyConstraint [] value();
}
}

这段代码定义了一个 MyConstraint(自定义注解)注解,它表示被注解的元素的值要小于等于指定的值。

Target:注解应用于的元素类型包括:方法、字段、注解类型、构造函数、参数、类型使用。

@Retention(RUNTIME) 表示该注解在运行时可见,这样就可以使用反射机制来读取注解信息。

@Repeatable(List.class) 表示此注解可以重复标注在同一元素上,而多次使用该注解时,需要使用外部注解 List 来包装并保存多个 Max 注解的数组。

@Documented 表示该注解会包含在JavaDoc中。

注解中的属性包括:

  • String message(),用于定义校验失败时的提示信息,默认为 “自定义校验错误信息”
  • Class<?>[] groups(),用于将校验信息分组,方便给不同的校验规则分配到不同的分组中;
  • Class<? extends Payload>[] payload(),用于在校验失败时传递的一些附加信息;

最后,该注解还内部定义了一个嵌套注解 List,用于表示被注解元素可以有多个MyConstraint(自定义注解)注解,通过 List 来包装这些注解,以此实现重复注解的功能。

我们还@MyConstraint 注解上加了 @Constraint(validatedBy = MyValidator.class) 注解,其中,validatedBy 属性指定了一个校验器类,该类需要实现 javax.validation.ConstraintValidator 接口,用于对注解进行具体的校验逻辑。

自定义校验器

自定义校验注解的目的是为了自定义校验规则,而自定义校验器则是实现这种自定义校验规则的具体方式,在自定义注解中指定自定义校验器可以使得该注解在被使用时自动调用相应的校验器进行参数校验。

上面自定义检验注解时指定了校验器为MyValidator,自定义校验器需要实现ConstraintValidator<A extends Annotation, T>这个接口,第一个泛型是校验注解,第二个是参数类型。

接下来我们实现自定义检验器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyValidator implements ConstraintValidator<MyConstraint, Object> {

@Override
public void initialize(MyConstraint constraintAnnotation) {
// 可以在这里初始化校验器
}

@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
// 在这里编写具体的校验逻辑
if (value == null) {
return true; // 如果校验的值为null,就不进行校验,交给@NotNull等其他校验注解处理
}
// 假设我们要校验字符串长度是否超过10个字符
String str = (String) value;
return str.length() <= 10;
}
}

该自定义校验器实现了 ConstraintValidator 接口,并通过泛型 ConstraintValidator<MyConstraint, Object>指定了被校验值的类型和自定义注解类型。在实现该接口的方法中,initialize方法可以用于从自定义注解中获取注解属性,而 isValid 方法则是实际进行校验的方法,其中第一个参数是被校验的值,第二个参数是校验上下文,可以用来设置校验失败时的错误信息。