SpringBoot-(二十二)Validator的参数校验与自定义注解实现参数、类校验

本文最后更新于:September 4, 2022 am

SpringBoot框架中有两个非常重要的策略:开箱即用和约定优于配置。其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。

目录

实现步骤

  • 一:写自定义注解。
  • 二:自定义实现校验规则。
  • 三:测试。

导入依赖

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

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

或者:

1
2
3
4
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>

自定义校验注解

以下内容均为常规配置,即模版。

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
package com.tothefor.annotation;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

/**
* @Author DragonOne
* @Date 2022/9/2 13:01
* @Title
* @Description
*/
@Documented
// 指定注解的校验由谁完成
@Constraint(validatedBy = {CheckPhoneValidator.class})
@Target(value = {ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPhone {
/**
* 默认提示信息
*
* @return
*/
String message() default "手机号格式不正确。";

/**
* 分组使用
*
* @return
*/
Class<?>[] groups() default {};

/**
* 在ValidatorFactory初始化期间定义约束验证器有效负载
*
* @return
*/
Class<? extends Payload>[] payload() default {};


@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
CheckPhone[] value();
}
}

编写校验实现类

这里的校验只是简单的进行了校验,可根据自定义进行校验。

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.tothefor.annotation;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;

/**
* @Author DragonOne
* @Date 2022/9/2 13:15
* @Title
* @Description
*/
public class CheckPhoneValidator implements ConstraintValidator<CheckPhone, String> {

private static final Pattern PHONE_CHECK = Pattern.compile("(?:0|86|\\+86)?1[3456789]\\d{9}");

/**
* initialize()方法使您可以访问已验证约束的属性值,并允许您将它们存储在验证器的字段中
* @param constraintAnnotation
*/
@Override
public void initialize(CheckPhone constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}

/**
* isValid()方法包含实际的验证逻辑
* @param phone
* @param context
* @return
*/
@Override
public boolean isValid(String phone, ConstraintValidatorContext context) {
if (phone == null || phone.length() < 11) {
return false;
}
return PHONE_CHECK.matcher(phone).matches();
}
}

测试校验

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
package com.tothefor.pojo.dto;

import com.tothefor.annotation.CheckPhone;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
* @Author DragonOne
* @Date 2022/9/2 13:56
* @Title
* @Description
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Person {
private String name;
private int age;
@CheckPhone
private String phone;
}

URL测试

1
2
3
4
5
6
@RequestMapping("/check")
public Object checkO(@RequestBody @Validated Person person){
return new JSONObject()
.fluentPut("check",person)
;
}

测试类

1
2
3
4
5
6
7
8
9
@Test
void testCheckPhone(){
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
Person p = new Person();
p.setPhone("324");
Set<ConstraintViolation<Person>> validate = validator.validate(p);
System.out.println(validate);
}

自定义类校验

注解

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
package com.tothefor.annotation;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

/**
* @Author DragonOne
* @Date 2022/9/2 14:40
* @Title
* @Description
*/
@Documented
// 指定注解的校验由谁完成
@Constraint(validatedBy = {CheckDateValidator.class})
@Target(value = {ElementType.METHOD, ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckDate {
/**
* 默认提示信息
*
* @return
*/
String message() default "不在活动时间内!";

/**
* 分组使用
*
* @return
*/
Class<?>[] groups() default {};

/**
* 在ValidatorFactory初始化期间定义约束验证器有效负载
*
* @return
*/
Class<? extends Payload>[] payload() default {};


@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
CheckDate[] value();
}
}

校验实现类

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
package com.tothefor.annotation;

import com.tothefor.pojo.dto.CheckDateDTO;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
* @Author DragonOne
* @Date 2022/9/2 14:42
* @Title
* @Description
*/
public class CheckDateValidator implements ConstraintValidator<CheckDate, CheckDateDTO> {


@Override
public void initialize(CheckDate constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}

@Override
public boolean isValid(CheckDateDTO date, ConstraintValidatorContext context) {
if (date == null) {
return false;
}
if (date.getNow().after(date.getStart()) && date.getNow().before(date.getEnd())) {
return true;
}
return false;
}
}

被校验类

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
package com.tothefor.pojo.dto;

import com.tothefor.annotation.CheckDate;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.Date;

/**
* @Author DragonOne
* @Date 2022/9/2 15:27
* @Title
* @Description
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@CheckDate
public class CheckDateDTO {
private Date start;
private Date end;
private Date now;
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testCheckDate(){
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
CheckDateDTO date = new CheckDateDTO();
date.setStart(DateUtilsLee.getTime("2022-08-20 22:12:12"));
date.setEnd(DateUtilsLee.getTime("2022-08-22 22:22:22"));
date.setNow(new Date());
Set<ConstraintViolation<CheckDateDTO>> validate = validator.validate(date);
System.out.println(validate);

}

加入全局异常处理器

异常捕获

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
package com.tothefor.DBTest;

import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

/**
* @Author DragonOne
* @Date 2022/8/20 14:17
* @墨水记忆 www.tothefor.com
*/

@RestControllerAdvice
public class GlobalExceptionHandler {

// Validator异常捕捉
@ExceptionHandler(MethodArgumentNotValidException.class)
public MethodArgumentNotValidException handler(MethodArgumentNotValidException e, HttpServletRequest request){
return e;
}

}

异常处理

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
package com.tothefor.DBTest;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
* @Author DragonOne
* @Date 2022/8/21 10:03
* @墨水记忆 www.tothefor.com
*/
@ControllerAdvice(basePackages = "com.tothefor.DBTest")
public class ResponseBody implements ResponseBodyAdvice<Object> {

@Autowired
private ObjectMapper objectMapper;

@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}

@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if(o==null){
return R.SUCCESS();
}
if(o instanceof BizErrorException){
BizErrorException eO = (BizErrorException) o;
return R.FAIL(eO.getCode(),eO.getException());
}
if(o instanceof MethodArgumentNotValidException){ // Validator异常
MethodArgumentNotValidException e = (MethodArgumentNotValidException)o;
return R.FAIL(BizErrors.ERRORS_Validator.code,BizErrors.ERRORS_Validator.exception);
}
if(o instanceof String){
return objectMapper.writeValueAsString(R.SUCCESS(o));
}
return R.SUCCESS(o);
}
}

异常码

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
package com.tothefor.DBTest;

import lombok.AllArgsConstructor;

/**
* @Author DragonOne
* @Date 2022/8/21 13:06
* @墨水记忆 www.tothefor.com
*
* 具体自定义异常状态码,可根据自行实现BizError进行自定义异常状态码
*/
@AllArgsConstructor
public enum BizErrors implements BizError {
ERROR_SYSTEM(1000000,"系统异常"),
ERRORS_Validator(100001,"参数异常"),
;

/**
* 异常码
*/
int code;
/**
* 异常信息
*/
String exception;

@Override
public int code() {
return code;
}

@Override
public String exception() {
return exception;
}
}

测试

1
2
3
4
5
6
@RequestMapping("/check")
public Object checkO(@RequestBody @Validated Person person){
return new JSONObject()
.fluentPut("check",person)
;
}

异常页面显示

1
2
3
4
5
6
7
{
"success": false,
"code": 100001,
"message": "参数异常",
"timestamp": 1662254126571,
"data": null
}

自定义提示信息

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
package com.tothefor.DBTest;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.List;

/**
* @Author DragonOne
* @Date 2022/8/21 10:03
* @墨水记忆 www.tothefor.com
*/
@ControllerAdvice(basePackages = "com.tothefor.DBTest")
public class ResponseBody implements ResponseBodyAdvice<Object> {

@Autowired
private ObjectMapper objectMapper;

@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}

@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if(o==null){
return R.SUCCESS();
}
if(o instanceof BizErrorException){
BizErrorException eO = (BizErrorException) o;
return R.FAIL(eO.getCode(),eO.getException());
}
if(o instanceof MethodArgumentNotValidException){
MethodArgumentNotValidException e = (MethodArgumentNotValidException)o;
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
StringBuilder sb = new StringBuilder();
for (FieldError it : fieldErrors){
sb.append(it.getField()+" ");
sb.append(it.getCode()+" ");
sb.append(it.getDefaultMessage()+" ");
}
return R.FAIL(BizErrors.ERRORS_Validator.code,sb.toString());
}
if(o instanceof String){
return objectMapper.writeValueAsString(R.SUCCESS(o));
}
return R.SUCCESS(o);
}
}

其中,处理提示信息为:(这部分可自行提取为一个方法进行处理)

1
2
3
4
5
6
7
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
StringBuilder sb = new StringBuilder();
for (FieldError it : fieldErrors){
sb.append(it.getField()+" ");
sb.append(it.getCode()+" ");
sb.append(it.getDefaultMessage()+" ");
}
  • getField():获取校验失败的参数名称。
  • getCode():获取校验的注解名称。
  • getDefaultMessage():获取注解的提示信息。@CheckPhone 获取的是注解默认的提示信息;@CheckPhone(“asdf”) 获取的提示信息为:”asdf”。