参数校验
# 参数校验
# 文档
参考官网
# 依赖引入
在 SpringBoot 2.3.x 以前 SpringBoot 包默认引入 spring-boot-starter-validation 包,而自 SpringBoot 2.3.x 以后官方将其排除,需要单独引入。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2
3
4
# @Valid & @Validated 🔥
参考博客
- javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果
- Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种)
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但分组、注解地方、嵌套验证等有所不同:
分组:
- @Valid:作为标准JSR-303规范,还没有吸收分组的功能。
- @Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制
注解地方:
@Valid:可以用在构造函数、方法、方法参数和成员属性(字段)上
@Validated:可以用在类、方法和方法参数上。但是不能用在成员属性(字段)上
两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。
嵌套(集联)验证(参考上面那个博客)
- @Validated和@Valid加在方法参数前,都不会自动对参数进行嵌套验证。能够用在成员属性(字段)上,提示验证框架进行嵌套验证
总结:
- 非成员属性(字段)直接使用 @Validated,开启校验
- 成员属性(字段)直接使用 @Valid,嵌套(集联)校验
注意,Validation 会将所有需要的都校验,即使第一个参数不通过,还是校验其他参数。所以此时需要把所有的错误消息都返回。
# Spring Validation
# @RequestParams 和 @PathVariables 参数的校验 🔥
一般情况下,校验的注解只能在实体类上标记,否则不起作用,但其实是没有在 Controller 上写 Spring 提供的 @Validated 注解,该注解用于开启控制器中的 @RequestParams 和 @PathVariables 的验证,方法级别上的校验
@RestController
@RequestMapping("/hello")
@Validated
public class HelloController {
@PostMapping("/test/{id}")
public ResponseEntity<List<User>> test(
@PathVariable @Range(min = 1, max = 10, message = "1~10哦!") Integer id,
@RequestBody User user) {
List<User> list = new ArrayList<>();
User user1 = User.builder()
.name("zhangsan")
.age(3L)
.build();
list.add(user1);
System.out.println("test");
return new ResponseEntity<>(list, HttpStatus.OK);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
当查询参数和路径参数都不符合校验规则,同时body参数也不符合校验规则,只会出现body校验规则的错误信息。如果body参数全部符合规则才会出现查询参数和路径参数校验失败信息。
注意,它会抛出 ConstraintViolationException 异常,可以在全局异常处理中捕获
/**
* 参数校验异常 URL 及查询参数(JSON 格式)
* 已经使用了 @ResponseStatus(HttpStatus.BAD_REQUEST) 所以无需返回 HttpEntity
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public UnifyResponse handleConstraintViolationException(ConstraintViolationException exception,
HttpServletRequest request,
HandlerMethod handlerMethod,
HttpMethod httpMethod) {
Set<ConstraintViolation<?>> constraintViolations = exception.getConstraintViolations();
String message = formatAllErrorsMessage(constraintViolations);
// String message = formatAllErrorsMessage(allErrors);
UnifyResponse unifyResponse = new UnifyResponse(10001, message, httpMethod + " " + request.getRequestURI());
log.warn("系统未知异常, 方法为:{}, 异常为:{}", handlerMethod, exception);
return unifyResponse;
}
/**
* 格式化参数校验错误信息
*/
private String formatAllErrorsMessage(Set<ConstraintViolation<?>> constraintViolations) {
StringJoiner stringJoiner = new StringJoiner(";");
constraintViolations.forEach(error -> {
String format = String.format("['%s'无法通过校验, %s]", error.getInvalidValue(), error.getMessage());
stringJoiner.add(format.toString());
});
return stringJoiner.toString();
}
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
# 表单提交,Bean 接收校验 🔥
@Data
public class User {
@NotNull(message = "id不能为空")
private Long id;
@NotNull(message = "年龄不能为空")
@Max(value = 35, message = "年龄不超过35")
@Min(value = 18, message = "年龄不小于18")
private Integer age;
}
2
3
4
5
6
7
8
9
10
11
@Slf4j
@RestController
public class UserController {
/**
* 如果都是用DTO包装参数,那么Controller可以不加@Validated(但建议还是都加上吧)
* 参数列表里用@Validated或@Valid都可以
*
* @param user
* @return
*/
@GetMapping("getUser")
public Result<User> getUser(@Validated User user) {
System.out.println("进来了");
return Result.success(null);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意,它会抛出 BindException 异常,可以在全局异常处理中捕获
/**
* 表单提交,Bean 接收
* 已经使用了 @ResponseStatus(HttpStatus.BAD_REQUEST) 所以无需返回 HttpEntity
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public UnifyResponse handleBindException(ConstraintViolationException exception,
HttpServletRequest request,
HandlerMethod handlerMethod,
HttpMethod httpMethod) {
Set<ConstraintViolation<?>> constraintViolations = exception.getConstraintViolations();
String message = formatAllErrorsMessage(constraintViolations);
UnifyResponse unifyResponse = new UnifyResponse(10001, message, httpMethod + " " + request.getRequestURI());
log.warn("系统未知异常, 方法为:{}, 异常为:{}", handlerMethod, exception);
return unifyResponse;
}
/**
* 格式化参数校验错误信息
*/
private String formatAllErrorsMessage(Set<ConstraintViolation<?>> constraintViolations) {
StringJoiner stringJoiner = new StringJoiner(";");
constraintViolations.forEach(error -> {
String format = String.format("['%s'无法通过校验, %s]", error.getInvalidValue(), error.getMessage());
stringJoiner.add(format.toString());
});
return stringJoiner.toString();
}
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
# @RequestBody 参数校验
@PostMapping("updateUser")
public Result<Boolean> updateUser(@Validated @RequestBody User user) {
System.out.println("进来了");
return Result.success(null);
}
2
3
4
5
注意,它会抛出 MethodArgumentNotValidException 异常,其实是BindException的子类,可以在全局异常处理中捕获
/**
* 参数校验异常 (JSON 格式)。MethodArgumentNotValidException 其实是 BindException 的子类
* 已经使用了 @ResponseStatus(HttpStatus.BAD_REQUEST) 所以无需返回 HttpEntity
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public UnifyResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException exception,
HttpServletRequest request,
HandlerMethod handlerMethod,
HttpMethod httpMethod) {
// 日!这获取到的 error 还是随机顺序 ...
List<ObjectError> allErrors = exception.getBindingResult().getAllErrors();
String message = formatAllErrorsMessage(allErrors);
UnifyResponse unifyResponse = new UnifyResponse(10001, message, httpMethod + " " + request.getRequestURI());
log.warn("系统未知异常, 方法为:{}, 异常为:{}", handlerMethod, exception);
return unifyResponse;
}
/**
* 格式化参数校验错误信息
*/
private String formatAllErrorsMessage(Set<ConstraintViolation<?>> constraintViolations) {
StringJoiner stringJoiner = new StringJoiner(";");
constraintViolations.forEach(error -> {
String format = String.format("['%s'无法通过校验, %s]", error.getInvalidValue(), error.getMessage());
stringJoiner.add(format.toString());
});
return stringJoiner.toString();
}
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
# 常用注解 🔥
@Range(min = 1, max = 10, message = "数字应为1~10哦!"):用于数字
@Length(min = 1, max = 10, message = "字符串长度应在1~10哦!"):用于字符串
@Validated
@NotNull
@NotBlank
@NotEmpty
@Positive
@Max
@Min
# 自定义校验注解 🔥
实体类
@Builder
@Getter
// @Setter
// @NoArgsConstructor
// @AllArgsConstructor
@PasswordEqualValid
public class User {
@Length(min = 3, max = 7, message = "字符串长度应在3~7哦!")
private String name;
@NonNull
private Long age;
@Length(min = 8, max = 16, message = "密码长度应在8~16位哦!")
private String password;
private String password2;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
自定义校验注解
/**
* 校验两次密码是否相同,应放在类上(需要操作对象的两个字段),必须有 password、password2 字段
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordEqualValidator.class)// 是数组,可以指定多个 validator。类似 @Import 关联模式
public @interface PasswordEqualValid {
String message() default "密码不相同";
/** 自定义校验注解必须加上的 */
Class<?>[] groups() default { };
/** 自定义校验注解必须加上的 */
Class<? extends Payload>[] payload() default { };
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
校验器
/**
* 泛型第二个参数用于该自定义注解修饰的目标类型,若是字段,则为字段的类型!
* 无需使用 @Configuration 等注解
*/
public class PasswordEqualValidator implements ConstraintValidator<PasswordEqualValid, User> {
private PasswordEqualValid passwordEqualValid;
/**
* 获取初始化时校验注解的信息
* @param constraintAnnotation 约束注解
*/
@Override
public void initialize(PasswordEqualValid constraintAnnotation) {
this.passwordEqualValid = constraintAnnotation;
}
@Override
public boolean isValid(User user, ConstraintValidatorContext constraintValidatorContext) {
String password = user.getPassword();
String password2 = user.getPassword2();
return password.equals(password2);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
测试
@RestController
@RequestMapping("/hello")
@Validated
public class HelloController {
@PostMapping("/test/{id}")
public ResponseEntity<List<User>> test(
@PathVariable @Range(min = 1, max = 10, message = "1~10哦!") Integer id,
@Length(max = 5, message = "姓名长度最大为5") String name,
@Validated Dog dog,// 这里不测试表单提交
@RequestBody @Validated User user) {
List<User> list = new ArrayList<>();
User user1 = User.builder()
.name("zhangsan")
.age(3L)
.build();
list.add(user1);
System.out.println("test");
return new ResponseEntity<>(list, HttpStatus.OK);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 嵌套校验
@Validated不支持嵌套校验,只能用@Valid
@Data
public class User {
@NotNull(message = "id不能为空")
private Long id;
@NotNull(message = "年龄不能为空")
@Max(value = 35, message = "年龄不超过35")
@Min(value = 18, message = "年龄不小于18")
private Integer age;
@NotNull(message = "所属部门不能为空")
@Valid
private Department department;
@Data
static class Department {
@NotNull(message = "部门编码不能为空")
private Integer sn;
@NotBlank(message = "部门名称不能为空")
private String name;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 分组校验
@Data
public class User {
@NotNull(message = "id不能为空", groups = {Update.class})
private Long id;
@NotNull(message = "年龄不能为空", groups = {Add.class, Update.class})
@Max(value = 35, message = "年龄不超过35", groups = {Add.class, Update.class})
@Min(value = 18, message = "年龄不小于18", groups = {Add.class, Update.class})
private Integer age;
public interface Add {
}
public interface Update {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@RestController
public class UserController {
@PostMapping("insertUser")
public Result<Boolean> insertUser(@Validated(User.Add.class) @RequestBody User user) {
System.out.println("进来了");
return Result.success(null);
}
@PostMapping("updateUser")
public Result<Boolean> updateUser(@Validated(User.Update.class) @RequestBody User user) {
System.out.println("进来了");
return Result.success(null);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
有两点需要注意:
- interface Add这些接口只是做个标记,本身没有任何实际意义,可以抽取出来,作为单独的接口复用
- interface Add还可以继承Default接口(Default Jakarta Bean Validation group.)
继承Default后,除非显示指定,否则只要加了@NotNull等注解,就会起效。但显示指定Group后,就按指定的分组进行校验。比如,上面的id只会在update时校验生效。 个人不建议继承Default,一方面是理解起来比较乱,另一方是加了Default后就无法进行部分字段更新了:
@PostMapping("updateUser")
public Result<Boolean> updateUser(@Validated(User.Update.class) @RequestBody User user) {
System.out.println("进来了");
return Result.success(null);
}
2
3
4
5
@Data
public class User {
@NotNull(message = "id不能为空", groups = {Update.class})
private Long id;
@NotNull(message = "年龄不能为空")
private Integer age;
@NotBlank(message = "住址不能为空")
private String address;
public interface Add extends Default {
}
public interface Update extends Default {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
此时如果想更新name,就不能只传id和name了,address也要传(默认也会校验)。当然,你也可以认为一般情况下update前都会有getById(),所以更新时数据也是全量的。
# List 校验
Spring Validation不支持以下方式校验:
@Data
public class User {
@NotNull(message = "id不能为空")
private Long id;
@NotNull(message = "年龄不能为空")
private Integer age;
}
2
3
4
5
6
7
8
9
@PostMapping("updateBatchUser")
public Result<Boolean> updateBatchUser(@Validated @RequestBody List<User> list) {
System.out.println(list);
return Result.success(null);
}
2
3
4
5
即使age不填,还是进来了,说明对于List而言,@Validated根本没作用
解决办法是,借鉴嵌套校验的模式,在List外面再包一层:
@PostMapping("updateBatchUser")
public Result<Boolean> updateBatchUser(@Validated @RequestBody ValidationList<User> userList) {
System.out.println(userList);
return Result.success(null);
}
2
3
4
5
public class ValidationList<E> implements List<E> {
@NotEmpty(message = "参数不能为空")
@Valid
private List<E> list = new LinkedList<>();
@Override
public int size() {
return list.size();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public boolean contains(Object o) {
return list.contains(o);
}
@Override
public Iterator<E> iterator() {
return list.iterator();
}
@Override
public Object[] toArray() {
return list.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}
@Override
public boolean add(E e) {
return list.add(e);
}
@Override
public boolean remove(Object o) {
return list.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return list.addAll(c);
}
@Override
public boolean addAll(int index, Collection<? extends E> c) {
return list.addAll(index, c);
}
@Override
public boolean removeAll(Collection<?> c) {
return list.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return list.retainAll(c);
}
@Override
public void clear() {
list.clear();
}
@Override
public E get(int index) {
return list.get(index);
}
@Override
public E set(int index, E element) {
return list.set(index, element);
}
@Override
public void add(int index, E element) {
list.add(index, element);
}
@Override
public E remove(int index) {
return list.remove(index);
}
@Override
public int indexOf(Object o) {
return list.indexOf(o);
}
@Override
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
@Override
public ListIterator<E> listIterator() {
return list.listIterator();
}
@Override
public ListIterator<E> listIterator(int index) {
return list.listIterator(index);
}
@Override
public List<E> subList(int fromIndex, int toIndex) {
return list.subList(fromIndex, toIndex);
}
public List<E> getList() {
return list;
}
public void setList(List<E> list) {
this.list = list;
}
}
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
实际开发时,建议专门建一个package存放Spring Validation相关的接口和类
# SpringValidatorUtils封装
public final class SpringValidatorUtils {
private SpringValidatorUtils() {}
/**
* 校验器
*/
private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
/**
* 校验参数
*
* @param param 待校验的参数
* @param groups 分组校验,比如Update.class(可以不传)
* @param <T>
*/
public static <T> void validate(T param, Class<?>... groups) {
Set<ConstraintViolation<T>> validateResult = validator.validate(param, groups);
if (!CollectionUtils.isEmpty(validateResult)) {
StringBuilder validateMessage = new StringBuilder();
for (ConstraintViolation<T> constraintViolation : validateResult) {
validateMessage.append(constraintViolation.getMessage()).append(" && ");
}
// 去除末尾的 &&
validateMessage.delete(validateMessage.length() - 4, validateMessage.length());
// 抛给全局异常处理
throw new ValidatorException(validateMessage.toString());
}
}
}
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
代码很简单,做的事情本质是和@Validated是一模一样的。@Validated通过注解方式让Spring使用Validator帮我们校验,而SpringValidatorUtils则是我们从Spring那借来Validator自己校验:
@PostMapping("insertUser")
public Result<Boolean> insertUser(@RequestBody User user) {
SpringValidatorUtils.validate(user);
System.out.println("进来了");
return Result.success(null);
}
2
3
4
5
6
此时不需要加@Validated。
买一送一,看看我之前一个同事封装的工具类(更加自由,调用者决定抛异常还是返回错误信息):
public final class ValidationUtils {
private static final Validator DEFAULT_VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
private ValidationUtils() {
}
/**
* 验证基于注解的对象
*
* @param target
*/
public static <T> String validateReq(T target, boolean throwException) {
if (null == target) {
return errorProcess("校验对象不能为空", throwException);
} else {
Set<ConstraintViolation<T>> constraintViolations = DEFAULT_VALIDATOR.validate(target);
ConstraintViolation<T> constraintViolation = Iterables.getFirst(constraintViolations, null);
if (constraintViolation != null) {
// 用户可以指定抛异常还是返回错误信息
return errorProcess(constraintViolation.getPropertyPath() + ":" + constraintViolation.getMessage(),
throwException);
}
}
return "";
}
private static String errorProcess(String errorMsg, boolean throwException) {
if (throwException) {
throw new InvalidParameterException(errorMsg);
}
return errorMsg;
}
}
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