java数据校验

简介

数据校验(Bean Validation)

  1. Bean Validation是将校验逻辑与相应的Bean域模型进行绑,是基于Bean的校验。
  2. 契约式编程:避免了if else 等硬编码校验(能通过契约约定解决的就不要去硬编码)。
  3. 面向接口编程:Bean Validation属于Java EE标准规范,是JSR(Java Specification Requests)抽象的具体实现,实际使用过程中仅需要面向标准使用即可。

img

JSR标准

JSR(Java Specification Requests),为需要校验的JavaBean,定义了源数据模型 和 API。

版本变迁:

JSR303,2009年,Java Bean Validation 1.0

  • 规定了Java数据校验的模型和API
  • 只支持对Java Bean进行校验

JSR349,2013年,Bean Validation 1.1,伴随着Java EE 7一起发布

  • 支持方法级验证(入参或返回值的验证)
  • Bean验证组件的依赖注入

JSR380,2019,Java Bean Validation 2.0和Jakarta Bean Validation 2.0(两者只有名字的区别),当前主流。

  • JDK最低版本要求:JDK 8
  • 一些新增的标准

依赖包

由Java Bean Validation变为Jakarta Bean Validation。(雅加达)

2018年03月, Oracle 把 JavaEE (Java Enterprise Edition)移交给开源组织 Eclipse 基金会,并把Java EE(包名javax)改为Jakarta EE。Eclipse接手后发布的首个Enterprise Java将是 Jakarta EE 9,该版本将以Java EE 8作为其基准版本(最低版本要求是Java8)

由于上述版本迁移问题,maven包由javaxjakarta下的两种坐标,但目前2.0.1版本下,内容完全一样:

  1. javax.validation到2.0.1.Final版本就彻底结束了,不再升级。
  2. jakarta.validation从2.0.1版本开始。
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>

<!-- https://mvnrepository.com/artifact/jakarta.validation/jakarta.validation-api -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.1</version>
</dependency>

目前最新版本是Jakarta Bean Validation 3.0,它唯一的变化,只是包名。

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/jakarta.validation/jakarta.validation-api -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.0</version>
</dependency>

不只是校验包,其他包名也有更改,如”javax.servlet.”改成了”jakarta.servlet.”

java.*:java SE的标准库,是对外承诺的java开发接口。会保持向后兼容,不会轻易修改。
javax.*: java的扩展包,如servelet/xml/validation等。属于某特定领域,不是普遍的api。被官方支持,兼容所有java支持的平台。

com.sun.*:不被官方支持,不推荐使用。是sun的hotspot虚拟机中java.*javax.*的实现类。因包含在rt中,故也可调用。但因为不是sun对外公开承诺的接口,所以根据实现的需要随时增减,不保证api跨平台(linux和windows的api可能不一样)、兼容版本(在不同版本的hotspot中可能不同)。
org.omg.*:大部分不是sun公司提供的,不具备向后兼容性,会根据需要随时增减。其中比较常用的是w3c提供的对XML、网页、服务器的类和接口。

HotSpot 虚拟机: 是Sun JDK和OpenJDK中所带的虚拟机,是目前使用范围最广的Java虚拟机。

起步案例

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.6.Final</version>
</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
public class Person {
@NotNull
public String name;

@NotNull
@Min(0)
public Integer age;
}

public static void main(String[] args) {
Person person = new Person();
person.setAge(-1);

// 1、使用【默认配置】得到一个校验工厂
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();

// 2、得到一个校验器
Validator validator = validatorFactory.getValidator();

// 3、校验Java Bean,解析注解返回校验结果
Set<ConstraintViolation<Person>> result = validator.validate(person);

// 输出校验结果
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
}

常见约束注解

多个可以组合发挥作用,如 @Min(value = 10,message = “年龄最小为10”)@Max(value = 100,message = “年龄最大为100”)。

除了内置校验,也可以自定义校验注解:

  1. 首先必须定义一个注解

  2. 然后才实现自定义校验类(具体逻辑)

BeanValidation内置

注解 说明
@Valid 被注释的元素是一个对象,需要检查此对象的所有字段值。适用于嵌套。@Valid注解用于验证级联的属性、方法参数或方法返回类型。比如你的属性仍旧是个Java Bean,你想深入进入校验它里面的约束,那就在此属性头上标注此注解即可。另外,通过使用@Valid可以实现递归验证,因此可以标注在List上,对它里面的每个对象都执行校验
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注释的元素的大小必须在指定的范围内@Size(min = 1,max = 10,message = “姓名长度必须为1到10”)
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期。必须是相对当前时间来讲“未来的”某个时间 @Future@JSONField(format=”yyyy-MM-dd HH:mm:ss”)private Date birth;
@Pattern(value) 被注释的元素必须符合指定的正则表达式
@GroupSequence 控制校验顺序。

HibernateValidator附加

也内置在springboot中了

注解 作用
@Email 被注释的元素必须是电子邮箱地址
@Length(min=, max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=, max=) 被注释的元素必须在合适的范围内
@GroupSequenceProvider 比@GroupSequence更强大的控制group调用注解(字段之间校验依赖)
@NotBlank 被注释的字符串的必须非空
@URL(protocol=,host=, port=, regexp=, flags=) 被注释的字符串必须是一个有效的url
@CreditCardNumber 被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性
@ScriptAssert(lang=, script=, alias=) 要有Java Scripting API 即JSR 223 (“Scripting for the JavaTM Platform”)的实现
@SafeHtml(whitelistType=, additionalTags=) classpath中要有jsoup包

非空判断注解的区别

  1. @NotNull 用在基本类型上,不能为null,但可以为空字符串
  2. @NotBlank 用在String上面,只能作用在String上,不能为null,而且调用trim()后,长度必须大于0(不能是空字符串)
  3. @NotEmpty用在集合类上面,不能为null,并且长度必须大于0、

kotlin的写法略有不同

1
2
3
4
5
For a field @field:NotBlank

For a getter @get:NotBlank

For a constructor @param:NotBlank

groups

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义
public interface IpRequired {}
// 应用到字段等
@NotNull(groups = IpRequired.class)
private Integer ipAddress;

// 调用

// 只校验ipAddress字段,其他不校验
public String getAllUserByName(@Validated(IpRequired.class) User user) {
return "nice";
}
public String getAllUserByBigAge(@Validated({CHECK.class, IpRequired.class}) User user) {
return "nice";
}

@GroupSequence

  • 在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证。
  • 控制group校验顺序,实现参数的顺序校验(默认情况下,不同group的约束校验无序)
  • Bean Validation 中内置的标准注解

顺序只能控制在分组级别,无法控制在约束注解级别。因为一个类内的约束(同一分组内),它的顺序是Set<MetaConstraint<?>> metaConstraints来保证的,所以可以认为同一分组内的校验器是没有有执行先后顺序的(不管是类、属性、方法、构造器…)

约束验证的顺序和短路能力,在某些场景十分重要:

  1. 第一个约束正确,第二个约束才有意义。
  2. 对于性能消耗非常大的约束,应该把它放到最后

实现GroupIpInfoBean中,先校验IAreaCkeckSequence,再校验GroupIpInfoBean自己。而IAreaCkeckSequence中,又先校验省IProvinceCheck,地市ICityCheck,再区县ICountyCheck。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface IProvinceCheck {}
public interface ICityCheck {}
public interface ICountyCheck {}

@GroupSequence({IProvinceCheck.class,ICityCheck.class,ICountyCheck.class})
public interface IAreaCkeckSequence {}

@GroupSequence({IAreaCkeckSequence.class,GroupIpInfoBean.class})
public class GroupIpInfoBean {

@EnumValidAnnotation(message = "所属省份不符合枚举要求",target = "area-province",field = "belongProvince",groups = {IProvinceCheck.class})
private String belongProvince;

@EnumValidAnnotation(message = "所属地市不符合枚举要求",target = "area-city",field = "belongCity",groups = {ICityCheck.class})
private String belongCity;

@EnumValidAnnotation(message = "所属区县不符合枚举要求",target = "area-county",field = "belongCounty",groups = {ICountyCheck.class})
private String belongCounty;

// 其他字段校验
}

@GroupSequenceProvider

多字段组合逻辑校验,若想借助@GroupSequence完成,相对来说还是比较困难的。

  • 比 @GroupSequence更强大,能够控制group校验,从而实现多种校验场景。
  • 比如多字段联合逻辑校验场景:当且仅当属性a值满足条件时,属性b的校验逻辑才生效
  • Hibernate Validator 附加的 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
33
// 定义
public class NetworkGroupSeqProvider implements DefaultGroupSequenceProvider<Network> {
@Override
public List<Class<?>> getValidationGroups(Network network) {
List<Class<?>> defaultGroupSeq = new ArrayList<>();
defaultGroupSeq.add(Network.class);

if (network != null) {
Integer protocolType = network.getProtocolType();
Integer circuitType = network.getCircuitType();

// 自定义校验条件
if (protocolType == null || circuitType == null
|| !(protocolType == 2 || circuitType == 2)
) {
// 必填
defaultGroupSeq.add(Network.IpRequired.class);
}
}

return defaultGroupSeq;
}
}
// 使用
@GroupSequenceProvider(NetworkGroupSeqProvider.class)
public class Network {
@NotNull(groups = IpRequired.class)
@Range(min = 0, max = 1, message = "是否固定IP取值不在0~1范围内", groups = IpRequired.class)
private Integer ipAddress;

public interface IpRequired {}

}

Bean Validation四种约束级别

  1. 字段约束(Field)
  2. 属性约束(Property)
  3. 容器元素约束(Container Element)
  4. 类约束(Class)

Bean Validation标准的约束全部支持1/2/3级别,全部不支持第4级。

作为补充的Hibernate-Validator提供了一些专门用于类级别的约束注解。

字段约束Field

最常见。

不支持对静态字段static的约束。

直接反射访问字段的值 -> Field#get(不会执行get方法体)。不会调用任何方法来进行校验,比如对应的get/set方法。

1
2
3
4
5
6
public class User {
@NotNull
@Size(min=3,max = 50)
private String name;
...
}
自定义字段注解
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
@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Constraint(validatedBy = {InStrListImp.class})
public @interface InStrList {

// 指定的有效值,多个使用,隔开
String values();

// 无效时的提示内容
String message() default "必须在逗号隔开的字符串列表中";

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

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

public class InStrListImp implements ConstraintValidator<InStrList, Object> {
private String values;

// 返回true表示符合逻辑
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
String[] valueArray = values.split(",");
Boolean isFlag = false;
for (int i = 0; i < valueArray.length; i++) {
// 存在一致就跳出循环
if (valueArray[i].equals(value)) {
isFlag = true;
break;
}
}
return isFlag;
}

@Override
public void initialize(InStrList inStrList) {
this.values = inStrList.values();
}
}

枚举自定义

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
// 使用
@ValidEnum(type = IfConstantsEnum.class, ignoreNull = false)
private Integer cakes;
// 枚举定义
public interface ICompareEnum {
Object getComparedValue();
}
public enum IfConstantsEnum implements ICompareEnum {
@Override
public Object getComparedValue() {
// 定义需要枚举校验的值
return this.index;
}
}
// 定义枚举注解
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { EnumValidator.class })
public @interface ValidEnum {
String message() default "枚举值不合法";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> type();
boolean ignoreNull() default true;
/**
* Defines several {@link ValidEnum} annotations on the same element.
*
* @see ValidEnum
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@interface List {
ValidEnum[] value();
}
}

public class EnumValidator implements ConstraintValidator<ValidEnum, Object> {
private Class<? extends Enum<?>> type;
private boolean ignoreNull;
@Override
public void initialize(final ValidEnum constraintAnnotation) {
this.type = constraintAnnotation.type();
this.ignoreNull = constraintAnnotation.ignoreNull();
}

@Override
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
if (type == null || (value == null && ignoreNull)) {
return true;
}
if (value == null) {
return false;
}
for (final Enum<?> element : type.getEnumConstants()) {
if (ICompareEnum.class.isAssignableFrom(type)) {
if (value.equals(((ICompareEnum) element).getComparedValue())) {
return true;
}
} else {
if (value.equals(element.name())) {
return true;
}
}
}
return false;
}
}

属性约束Property

若一个Bean遵循Java Bean规范(getter/setter),则可使用属性约束来代替字段约束。

会调用属性get方法 -> getXXX(会执行get方法体)来获取待校验的值。

  1. 约束放在get方法上优于放在set方法上,这样只读属性(没有set方法)依然可以执行约束逻辑
  2. 不要在属性和字段上都标注注解,否则会重复执行约束逻辑(有多少个注解就执行多少次)
  3. 不要既在属性的get方法上又在set方法上标注约束注解。

如果希望执行了验证就输出一句日志,又或者POJO被字节码增强了,建议使用属性约束。

字节码增强:https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html

1
2
3
4
5
6
7
8
public class User {
private String name;

@NotNull
public String getName() {
return this.name;
}
}

容器元素级别约束Container Element

容器,List、Set、Map等。

验证容器内(每个)元素,形如List<User>希望验证List容器内部元素User。

必须基于Java Bean,验证才会生效,所以需要将容器(List等),封装成Bean来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义
public class UserList {
// 对于Hibernate-Validator6.0之前,验证容器元素时必填@Valid,后续版本非必填
// 支持Bean Validator和Hibernate-Validator
private List<@Valid @NotNull User> userList;
public UserList(List<@Valid @NotNull User> userList) {
this.userList = userList;
}
}

// 使用
List<@NotNull User> userList = new ArrayList<>();
userList.add(null);
userList.add(new User());
User user = new User();
user.setName="haha";
userList.add(user);

// 必须基于Java Bean,验证才会生效
UserList userListValidate = new UserList(userList);
自定义容器元素

某些容器不被支持校验,如Result,可以自定义容器元素。

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
// 首先注册一个值提取器,ValueExtractor
public class ResultValueExtractor implements ValueExtractor<Result<@ExtractedValue ?>> {

@Override
public void extractValues(Result<?> originalValue, ValueReceiver receiver) {
receiver.value(null, originalValue.getData());
}
}

public class ResultDemo {
public Result<@Valid User> userResult;
}

User user = new User();
user.name = "nice";
Result<User> result = new Result<>();
result.setData(result);

// 把Result作为属性放进去
ResultDemo resultDemo = new ResultDemo();
resultDemo.userResult = result;

// 注册自定义的值提取器
Validator validator = ValidatorUtil.obtainValidatorFactory()
.usingContext()
//将自定义值提取器注册到Validator中
.addValueExtractor(new ResultValueExtractor())
.getValidator();

ValidatorUtil.printViolations(validator.validate(resultDemo));
}

SpringBoot实现

需要自行提供一个验证器来覆盖掉自动装配进去的,可参考ValidationAutoConfiguration

1
2


类约束Class

所有的约束注解都会执行,不存在短路效果

@ScriptAssert

@ScriptAssert对null值并不免疫,都会执行,因此书写脚本时注意判空

语义上不够明显,需要阅读脚本才知,推荐使用在验证逻辑只用一次(只一个地方使用)且简单(比如只是简单判断而已)的情况。

1
2
3
4
5
6
7
8
9
10
11
@ScriptAssert(lang = "javascript", alias = "_", script = "_.maxStuNum >= _.studentNames.length")

public class Room {
@Positive
private int maxStuNum;
@NotNull
private List<String> studentNames;
}

// 调用
.validate(room)
自定义类注解
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
// 类注解
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = {ValidStudentCountConstraintValidator.class})
public @interface ValidStudentCount {
String message() default "学生人数超过最大限额";
// String message() default "{com.example.validation.ValidAddress.message}";

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

public class ValidStudentCountConstraintValidator implements ConstraintValidator<ValidStudentCount, Room> {

@Override
public void initialize(ValidStudentCount constraintAnnotation) {
}

@Override
public boolean isValid(Room room, ConstraintValidatorContext context) {
if (room == null) {
return true;
}
boolean isValid = false;
if (room.getStudentNames().size() <= room.getMaxStuNum()) {
isValid = true;
}

// 自定义提示语(当然你也可以不自定义,那就使用注解里的message字段的值)
if (!isValid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("校验失败xxx")
.addPropertyNode("studentNames")
.addConstraintViolation();
}
return isValid;
}
}

// 使用
@ValidStudentCount
public class Room {}

SpringBoot校验应用场景

上述已经定义好的四种约束,如何使用呢?

备注:java代码的侵入性强。可以通过两种AOP方式减少侵入:

  1. 基于Java EE的@Inteceptors实现
  2. 基于Spring Framework实现,此处spring boot为例。

Springboot默认集成了BeanValidation和HibernateValidator校验。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

https://github.com/Snailclimb/springboot-guide/blob/master/docs/advanced/spring-bean-validation.md

@Validated

在Controller 中 请求参数上添加@Validated 标签开启验证

与@Valid异同: @Validated是Spring对@Valid(javax.validation.Valid)的一层封装.

场景 @Validated @Valid
controller验证Request Body @Validated @Valid 抛出MethodArgumentNotValidException,Spring默认转为400(Bad Request)请求
controller验证PathVariable/RequestParam 类上标注@Validated,并方法参数声明需要的约束注解 抛出ConstraintViolationException
Service验证,需要组合两个注解使用 类上标注@Validated 并方法参数@Valid
Bean字段属性上,嵌套校验 @Valid
构造器 @Valid
@Validated
分组 @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
// @validated和@valid只能在controller层的参数前面
// Service验证,需要组合两个注解使用示例
@Service
@Validated
public class UserService {
// 该请求只校验没分组的字段,school
public String getAllUser(@Valid User user) {

System.out.println("nicecccccccc");
return "nice";
}
}

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;

@Test
public void valid() {
com.lean.https.dto.User user = new User();
user.setAge(23);
userService.getAllUser(user);
}
}

报错:

javax.validation.ConstraintViolationException: getAllUser.user.school: 不能为空, getAllUser.user.card: 请输入card
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
@Validated
@Service
public class BrandServiceImpl extends ServiceImpl<BrandMapper, Brand> implements BrandService {

@Override
public boolean saveBrand(@Valid Brand brand) {
return save(brand);
}

@Override
public boolean updateBrand(@Valid Brand brand) {
return updateById(brand);
}
// -----------------------------------------------

@Override
public Brand getBrandByCode(@NotBlank String brandCode) {
return getById(brandCode);
}

@Override
@Validated({ValidGroup.class})
public boolean saveBrand(@Valid Brand brand) {
return save(brand);
}

@Override
@Validated({ValidGroup.class})
public boolean updateBrand(@Valid Brand brand) {
return updateById(brand);
}

路径校验(PathVariable)

可以对url地址路径进行正则表达式的校验

  1. 语法:路径变量:正则表达式
  2. 当URI不满足正则表达式时,客户端将收到404错误码
  3. 而不是通过捕获异常的方式,向前端返回统一的、自定义格式的响应参数。
1
2
3
4
5
6
7
@GetMapping("detail/{domain:[a-zA-Z]+}/{userId}")
public String getPath(@PathVariable("domain") String group, @PathVariable("userId") Integer userId) {
return "nice";
}

// 访问:https://localhost/detail/a/2
// 返回404

方法参数校验

  1. 基本类型参数校验(RequestParam):需要所在类上增加注解@Validated
1
2
3
4
5
6
7
8
9
10
11
12
@Validated
public class user {
@RequestMapping(value = "basic", method = RequestMethod.GET)
// 该请求需要校验name,age
public String getByAge(
@Size(min = 1, max = 10, message = "姓名字符串的长度为1到10") @RequestParam("name") String name,
@Min(value = 10, message = "年龄最小为10") @Max(value = 100, message = "年龄最大为100") @RequestParam("age") Integer age) {
return "nice";
}
}
// 访问https://localhost/basic?name=12345678901234567890&age=2
// 返回报错:javax.validation.ConstraintViolationException: getByAge.age: 年龄最小为10, getByAge.name: 姓名字符串的长度为1到10
  1. Bean参数校验
1
2
3
4
5
6
7
8
9
10
11
//Java Bean作为入参校验

public void save(@NotNull @Valid Person person) throws NoSuchMethodException {
Method currMethod = this.getClass().getMethod("save", Person.class);
Set<ConstraintViolation<PersonService>> validResult = obtainExecutableValidator().validateParameters(this, currMethod, new Object[]{person});
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
throw new IllegalArgumentException("参数错误");
}
}
  1. Bean返回值校验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//校验方法返回值


public @NotNull Person getOne(@NotNull @Min(1) Integer id, String name) throws NoSuchMethodException {

// ... 模拟逻辑执行,得到一个result
Person result = null;



// 在结果返回之前校验
Method currMethod = this.getClass().getMethod("getOne", Integer.class, String.class);

Set<ConstraintViolation<PersonService>> validResult = obtainExecutableValidator().validateReturnValue(this, currMethod, result);

if (!validResult.isEmpty()) {

// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
throw new IllegalArgumentException("参数错误");
}
return result;

}
  1. 注解应该写在接口的方法内上还是实现的方法内?

    • 如果该方法是接口方法的实现,那么可存在如下两种case:
      • 保持和接口方法一毛一样的约束条件(极限情况:接口没约束注解,那你也不能有)
      • 实现类一个都不写约束条件,结果就是接口里有约束就有,没约束就没有

@Validated注解应该放在接口(方法)上,还是实现类(方法)上?

1
2
3
4
5
6
7
8
9
10
11
12
13
@NotNull加到接口,@Validated加到[实现]或者接口都可以

那么,@Validated加到哪里比较好呢?

放到实现上的原因:

(1).更灵活,如果一个接口多个实现的话,需要校验的实现可以对其进行校验,不需要校验的就不用校验参数

(2).避免坑,如果实现和接口是在不同的maven项目下,接口就可以不用引用hibernate-validator这个包,避免包冲突的坑

(3).更符合规范,接口是定义方法的规范,@Validated是实现校验,应该放到实现中

综上,@Validated放到实现上,@NotNull,@Valid等声明放到接口上

对象校验(bean)

详见Bean Validation四种约束级别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 字段约束示例
public class User {
@NotNull
private String name;

@NotNull
@Min(value = 3)
private Integer age;

@Pattern(regexp ="^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
private Integer phone;

}
// 调用 @Validated也可以忽略
public String getAllUser(@Validated User user) {
return "nice";
}

分组校验(Group)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class User {
// 设置归属组
@NotNull(groups = NAME.class)
private String name;

@NotNull
@Min(value = 3, groups = AGE.class)
private Integer age;

// 定义分组名
public interface NAME {};
public interface AGE {};
}

// 使用 @Validated({NAME.class, AGE.class},只校验这两分组,User其他字段不校验
public String getAllUser(@Validated({NAME.class, AGE.class}) User user) {
return "nice";
}

全局异常拦截器

全局异常处理器,将Controller层异常统一封装,并向前端返回校验失败信息。

  • 优点:
    • 将 Controller 层的异常和数据校验的异常进行统一处理,减少模板代码,减少编码量,提升扩展性和可维护性。
    • 去掉了try catch高耦合代码
    • 统一封装,返回给前端一个友好的界面
    • 避免了层层向上抛出异常
  • 缺点:只能处理 Controller 层未捕获(往外抛)的异常,对于 Interceptor(拦截器)层的异常,Spring 框架层的异常,就无能为力了

实现方式:

  1. Spring的AOP(复杂)
  2. @ControllerAdvice结合@ExceptionHandler(简单)

@ControllerAdvice

@ControllerAdvice注解源码中被@Component标记,所以可以被组件扫描到Spring容器。

1
2
3
4
5
6
7
8
9
10
// controller通知器
@ControllerAdvice
public class ControllerExceptionAdvice {
// 异常类型对应的处理方法
@ExceptionHandler
// 返回值:给前端的是个String类型
public String handleValidationException(final Exception e) {
return "error";
}
}

@ControllerAdvice使用形式

1
2
3
4
5
6
7
8
// 全局使用
@ControllerAdvice

// 只对某个包下面的controller进行通知
@ControllerAdvice(basePackages = "com.example.services.nice")

// 只对某个类(或多个类)下面的controller进行通知
@ControllerAdvice(basePackageClasses = { MyTokenController.class })

@RestControllerAdvice

1
2
3
@RestControllerAdvice = @ControllerAdvice + @ResponseBody

方法返回值默认作为HTTP Body处理,即默认增加了@ResponseBody注解。

顺序:如果有多个@ControllerAdvice注解类,当第一个加载的注解类里有对需要捕获异常的相同类/父类有方法处理,就会使用第一个处理方法。可@Order指定顺序。

@ExceptionHandler

所有异常

1
@ExceptionHandler

ConstraintViolationException

  1. 指定参数为方法的基本参数异常
  2. 参数前有@RequestParam
  3. http://localhost/user?age=3&name=nice中的age和name校验异常
1
@ExceptionHandler(value = ConstraintViolationException.class)

MethodArgumentNotValidException

  1. 指定参数为实体类时异常
  2. 实体类前必须有@RequestBody,才能捕获
  3. Content-Type为application/json

BindException

  1. 指定参数为实体类时异常
  2. 实体类前没有有@RequestBody,但的确是个实体类
  3. Content-Type为application/x-www-form-urlencoded,即表单请求

这两者一般会合起来处理。

1
2
3
4
5
6
7
8
9
10
11
public String getAllUser(@Validated User user) {}
@ExceptionHandler(BindException.class)

public String getAllUser(@Validated @RequestBody User user) {}
@ExceptionHandler({MethodArgumentNotValidException.class})

// 合并写法
@ExceptionHandler({MethodArgumentNotValidException.class,BindException.class})
List<ObjectError> objectErrors = e instanceof BindException
? ((BindException) e).getBindingResult().getAllErrors()
: ((MethodArgumentNotValidException) e).getBindingResult().getAllErrors();

自定义异常

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
// 捕获
@ExceptionHandler(value = ElegantException.class)
@ResponseBody
public ResultVO elegantExceptionHandle(ElegantException e) {
return ResultVOUtil.error(e.getCode(), e.getMessage());
}

// 定义异常
public class ElegantException extends RuntimeException {

private Integer code;

public ElegantException(ResultEnum resultEnum) {
super(resultEnum.getMessage());
this.code = resultEnum.getCode();
}

public ElegantException(Integer code, String message) {
super(message);
this.code = code;
}
}
// 调用
if (false) {
throw new ElegantException(ResultEnum.PARAM_ERROR);
}

返回值

封装返给前端的内容。

包括如下内容:

  1. BaseResponseEnum.java 编码常量
  2. ResponseUtil.java 包装类
  3. 其他关于返回值的特定封装类:
    • 自定义ParameterException类,封装参数异常返回
    • 自定义UserAppException类,封装用户程序应用异常类
BaseResponseEnum.java 编码常量
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
public interface IResponse {
String getCode();
String getMessage();
}

public enum BaseResponseEnum implements IResponse {
SUCCESS("SC_200", "成功"),
PARAMETER_EXCEPTION("SC_400", "参数校验错误"),
AUTHORITY_EXCEPTION("SC_403", "权限不足"),
SYSTEM_EXCEPTION("SC_500", "系统错误"),
BUSINESS_EXCEPTION("SC_500", "业务错误");

private String code;
private String message;

BaseResponseEnum(final String code, final String message) {
this.code = code;
this.message = message;
}

@Override
public String getCode() {
return this.code;
}

@Override
public String getMessage() {
return this.message;
}
}
ResponseUtil.java 包装类
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
public final class ResponseUtil {
// 默认构造器
private ResponseUtil() {
}
// 返回成功
// return ResponseUtil.wrapSuccess();
public static ResponseDto<Void> wrapSuccess() {
return new ResponseDto<Void>(BaseResponseEnum.SUCCESS);
}
// 返回成功 + 内容
// return ResponseUtil.wrapSuccess(new UserDto())
public static <T> ResponseDto<T> wrapSuccess(final T body) {
return new ResponseDto<T>(BaseResponseEnum.SUCCESS, body);
}

// 返回成失败
public static ResponseDto<Void> wrapException(final String code, final String message) {
return new ResponseDto<Void>(code, message);
}
// ApplicationException,用户定义应用程序异常
// return IrmsResponseUtil.wrapException(e)
public static ResponseDto<Void> wrapException(final ApplicationException e) {
return new ResponseDto<Void>(e);
}

// Exception,所有异常
public static ResponseDto<Void> wrapException(final Exception e) {
return wrapException(new SystemException(e));
}
}
UserAppException自定义异常类(用户程序应用异常类)
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
public class UserAppException extends RuntimeException implements IResponse {
private final String code;
private String message;

public ApplicationException() {
super(BaseResponseEnum.SYSTEM_EXCEPTION.getMessage());
this.code = BaseResponseEnum.SYSTEM_EXCEPTION.getCode();
this.message = BaseResponseEnum.SYSTEM_EXCEPTION.getMessage();
}

public ApplicationException(final String message) {
super(message);
this.code = BaseResponseEnum.SYSTEM_EXCEPTION.getCode();
}

public ApplicationException(final String code, final String message) {
super(message);
this.code = code;
this.message = message;
}

@Override
public String getCode() {
return code;
}

@Override
public String getMessage() {
return message;
}
}
ParameterException类
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
public class ParameterException extends UserAppException {
public static final String DEFAULT_PARAM_CODE = BaseResponseEnum.PARAMETER_EXCEPTION.getCode();

public static final String DEFAULT_PARAM_MESSAGE = BaseResponseEnum.PARAMETER_EXCEPTION.getMessage();

private final String code;
private final String message;

public ParameterException() {
super();
this.code = DEFAULT_PARAM_CODE;
this.message = DEFAULT_PARAM_MESSAGE;
}

public ParameterException(final String message) {
super(DEFAULT_PARAM_CODE, message);
this.code = DEFAULT_PARAM_CODE;
this.message = message;
}

public ParameterException(final String code, final String message) {
super(code, message);
this.code = code;
this.message = message;
}

@Override
public String getCode() {

return code;
}

@Override
public String getMessage() {

return message;
}
}

拦截案例

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
@ControllerAdvice
public class ControllerExceptionAdvice {
private static final Logger LOGGER = LoggerFactory.getLogger(ControllerExceptionAdvice.class);
// 实体类参数校验
@ExceptionHandler({ BindException.class, MethodArgumentNotValidException.class })
@ResponseBody
public ResponseDto<Void> handleValidationException(final Exception e) {
List<FieldError> errors = null;
if (e instanceof BindException) {
errors = ((BindException) e).getFieldErrors();
} else if (e instanceof MethodArgumentNotValidException) {
errors = ((MethodArgumentNotValidException) e).getBindingResult().getFieldErrors();
}
LOGGER.error(e.getMessage(), e);
return ResponseUtil.wrapException(new ParameterException(this.getFieldErrorString(errors)));
}

// 捕获前端发送过来的数据无法被正常处理(比如,后端希望json格式,前端却非json)
@ExceptionHandler({ HttpMessageNotReadableException.class })
@ResponseBody
public ResponseDto<Void> handleValidationException(final HttpMessageNotReadableException e) {
return ResponseUtil.wrapException(new ParameterException(e.getMessage()));
}

// 自定义用户程序异常
@ExceptionHandler({ UserAppException.class })
@ResponseBody
public ResponseDto<Void> handleUserAppException(final UserAppException e) {
return ResponseUtil.wrapException(e);
}

// 所有异常
@ExceptionHandler({ Exception.class })
@ResponseBody
public ResponseDto<Void> handleException(final Exception e) {
LOGGER.error(e.getMessage(), e);
return ResponseUtil.wrapException(e);
}

@ExceptionHandler({ ClientAbortException.class })
@ResponseBody
public ResponseDto<Void> clientAbortException(final ClientAbortException e) {
LOGGER.warn(e.getMessage(), e);
return ResponseUtil.wrapException(e);
}

@SuppressWarnings("nls")
private String getFieldErrorString(final List<FieldError> errors) {
final StringBuilder sb = new StringBuilder();
for (final FieldError error : errors) {
sb.append(error.getField() + ": " + error.getDefaultMessage() + "! ");
}
return sb.toString();
}
}

工具方法

因为Validator等校验器是线程安全的,因此一般来说一个应用全局仅需一份、只初始化一次。可以封装这些方法作为工具方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class ValidatorUtil {

public static ValidatorFactory obtainValidatorFactory() {
return Validation.buildDefaultValidatorFactory();
}

public static Validator obtainValidator() {
return obtainValidatorFactory().getValidator();
}

public static ExecutableValidator obtainExecutableValidator() {
return obtainValidator().forExecutables();
}

public static <T> void printViolations(Set<ConstraintViolation<T>> violations) {
violations.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
}

}

一些核心思想

Validator

校验器,可实现对Java Bean、某个属性、方法、构造器等完成校验。

方法 描述
validate 校验Java Bean上的所有约束,包括属性约束 + 类的约束。.validate(user)
validateProperty 校验指定的属性。.validateProperty(user, “fullName”)
validateValue 校验指定value值,是否符合指定属性上的所有约束。不需要存在对象实例,直接校验某个值是否满足某个属性的所有约束,可以做事前的校验判断。 .validateValue(User.class, “fullName”, “want”)
getConstraintsForClass 获取Class类型描述。.getConstraintsForClass(User.class)
forExecutables 获得Executable校验器,只能校验Java Bean

使用案例:

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
public class Network {
@NotNull
@Range(min = 0, max = 5, message = "组网模式取值不在0~5范围内")
private Integer circuitType;

public @NotBlank(message = "不能为空的字符串") String getDrive(@Max(75) int speedInMph) {
return "";
}
}

// 测试
public class ValidGroup {
public static void main(String[] args) {
Network network = new Network();
network.setCircuitType(null);

Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

// validate实例对象
Set<ConstraintViolation<Network>> networkViolation = validator.validate(network);
networkViolation.stream().map(v ->
v.getPropertyPath() + v.getMessage() + ":" + v.getInvalidValue()
).forEach(System.out::println);

// validate实例某一个属性
Set<ConstraintViolation<Network>> networkProperty = validator.validateProperty(network, "circuitType");
networkProperty.stream().map(s -> s.getPropertyPath() + s.getMessage() + ":" + s.getInvalidValue()).forEach(System.out::println);

// validate实例某一个属性值校验
Set<ConstraintViolation<Network>> networkValue = validator.validateValue(Network.class, "circuitType", 10);
networkValue.stream().map(s -> s.getPropertyPath() + s.getMessage() + ":" + s.getInvalidValue()).forEach(System.out::println);

// 获取Class类型描述
BeanDescriptor beanDescriptor = validator.getConstraintsForClass(Network.class);
beanDescriptor.getConstrainedProperties(); // 被约束的field列表
beanDescriptor.getConstrainedMethods(MethodType.GETTER); // 被约束的属性列表(get方法)
beanDescriptor.getConstrainedConstructors();
beanDescriptor.getConstraintsForProperty("circuitType");
beanDescriptor.isBeanConstrained();
beanDescriptor.getConstraintDescriptors().size();

// 获得校验执行器,可以对方法输入参数、方法返回值进行约束校验
ExecutableValidator executableValidator = validator.forExecutables();

try {
Object[] parameterValues = {80};
// 对方法的输入参数进行校验
Set<ConstraintViolation<Network>> networkReqList = executableValidator.validateParameters(
network,
network.getClass().getDeclaredMethod("getDrive", int.class),
parameterValues
);
networkReqList.stream().map(s -> s.getPropertyPath() + s.getMessage() + ":" + s.getInvalidValue()).forEach(System.out::println);

// 对方法的返回值进行校验
Method method = network.getClass().getDeclaredMethod("getDrive", int.class);
Set<ConstraintViolation<Network>> networkRespList = executableValidator.validateReturnValue(
network,
method,
method.invoke(network, parameterValues)
);
networkRespList.stream().map(s -> s.getPropertyPath() + s.getMessage() + ":" + s.getInvalidValue()).forEach(System.out::println);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}

ConstraintViolation

约束校验失败的详情信息。违反一个约束,就生成一个实例。

1
2
3
4
Set<ConstraintViolation<Network>> networkViolation = validator.validate(network);
networkViolation.stream().map(v ->
v.getPropertyPath() + v.getMessage() + ":" + v.getInvalidValue()
).forEach(System.out::println);

ValidatorContext

可以定制设置校验器的核心组件(提供了Validator校验器的五大核心组件可以定制)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface ValidatorContext {
// 对message内容进行格式化,若有占位符{}或者el表达式${},就执行替换和计算
// 如@NotNull(message="{username.size}") username.sizes是配置文件中的变量
ValidatorContext messageInterpolator(MessageInterpolator messageInterpolator);

// 确定某个属性是否能被ValidationProvider访问
ValidatorContext traversableResolver(TraversableResolver traversableResolver);

// @Constraint(validatedBy = { xxx.class })。
ValidatorContext constraintValidatorFactory(ConstraintValidatorFactory factory);

// 获取方法/构造器的参数名
ValidatorContext parameterNameProvider(ParameterNameProvider parameterNameProvider);

// 提供一个Clock,给@Past、@Future等阅读判断提供参考。唯一实现为DefaultClockProvider
ValidatorContext clockProvider(ClockProvider clockProvider);

// 添加
ValidatorContext addValueExtractor(ValueExtractor<?> extractor);
// 得到一个定制化的校验器
Validator getValidator();
}

ValueExtractor

值提取器,2.0版本新增,用于把值从容器内提取出来参与校验(容器包括数组、集合、Map、Optional等)

所以从Bean Validation2.0开始就支持验证容器内的元素,形如:List<@NotNull @Valid Person>、Optional<@NotNull @Valid Person>

可以自定义ValueExtractor来实现自定义容器值提取器。

  1. 定义
  2. 添加ValidatorContext#addValueExtractor()
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
// 自定义容器Result
public final class Result<T> implements Serializable {
private boolean success = true;
private T data = null;
private String errCode;
private String errMsg;
}

// 注册到值提取器,ValueExtractor
public class ResultValueExtractor implements ValueExtractor<Result<@ExtractedValue ?>> {
@Override
public void extractValues(Result<?> originalValue, ValueReceiver receiver) {
receiver.value(null, originalValue.getData());
}
}

public class ResultDemo {
public Result<@Valid User> userResult;
}

// 调用
User user = new User();
user.name = "nice";
Result<User> result = new Result<>();
result.setData(result);

// 把Result作为属性放进去
ResultDemo resultDemo = new ResultDemo();
resultDemo.userResult = result;

// 注册自定义的值提取器
Validator validator = ValidatorUtil.obtainValidatorFactory()
.usingContext()
//将自定义值提取器注册到Validator中
.addValueExtractor(new ResultValueExtractor())
.getValidator();

ValidatorUtil.printViolations(validator.validate(resultDemo));
}

自定义的Validator

利用ValidatorContext,要想定制设置核心组件,实现自定义的Validator,两种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// new实例的方式,不够抽象,强耦合了Hibernate Validator的API(ValidatorContextImpl)
@Test
public void test2() {
// 先使用默认的Context上下文,并且初始化一个Validator实例
ValidatorFactoryImpl validatorFactory = (ValidatorFactoryImpl) ValidatorUtil.buildDefaultValidatorFactory();
// 必须传入一个校验器工厂实例
ValidatorContext validatorContext = new ValidatorContextImpl(validatorFactory)
.parameterNameProvider(new DefaultParameterNameProvider())
.clockProvider(DefaultClockProvider.INSTANCE);

// 通过该上下文,生成校验器实例(注意:调用多次,则生成多个实例)
return validatorContext.getValidator();
}
// 工厂生成方式,推荐,主要是使用ValidatorFactory中的usingContext方法
@Test
public void test3() {
Validator validator = Validation.buildDefaultValidatorFactory().usingContext()
.parameterNameProvider(new DefaultParameterNameProvider())
.clockProvider(DefaultClockProvider.INSTANCE)
.getValidator();
}

获得Validator实例的两种办法

  1. 工厂直接获取,默认
1
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
  1. 从上下文获取,可以任意指定核心组件实现,实现定制。
1
2
3
4
Validator validator = Validation.buildDefaultValidatorFactory().usingContext()
.parameterNameProvider(new DefaultParameterNameProvider())
.clockProvider(DefaultClockProvider.INSTANCE)
.getValidator();

ValidationMessages.properties

  1. 可以把校验消息都配置到.properties文件中。默认目录是resources/ValidationMessages.properties

    (spring boot自动读取classpath中的ValidationMessages.properties)

  2. 当验证不通过时会抛出 ValidationMessages.properties 中配置的提示信息。

  3. 若想自定义文件名,如 messages.properties,可以做路径配置即可。

1
2
3
4
5
6
7
8
9
10
11
<resource>
<directory>src/main/resources</directory>
<includes>
<include>app/build/**/*</include>
<include>messages.properties</include>
<include>WEB-INF/*</include>
</includes>
<excludes>
<exclude>node/**/*</exclude>
</excludes>
</resource>

内容:

key=value(value即消息,默认是ASCII编码),需要修改为UTF-8格式编码

img

在这里插入图片描述

使用示例

1
2
3
4
5
6
// Bean中使用注解
@NotNull(message = "{user.card}")
private String card;

// resources/ValidationMessages.properties文件中,设置校验异常消息
user.card=请输入card

参考

不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知

https://blog.csdn.net/f641385712/article/details/108184782

Bean Validation声明式校验方法的参数、返回值

https://blog.csdn.net/f641385712/article/details/108256131

站在使用层面,Bean Validation这些标准接口你需要烂熟于胸

https://blog.csdn.net/f641385712/article/details/108342248

Validator校验器的五大核心组件,一个都不能少

https://blog.csdn.net/f641385712/article/details/108358583

Bean Validation声明式验证四大级别:字段、属性、容器元素、类

https://blog.csdn.net/f641385712/article/details/109234678

自定义容器类型元素验证,类级别验证(多字段联合验证)

https://blog.csdn.net/f641385712/article/details/109270066

SpringBoot里参数校验/参数验证

https://blog.csdn.net/jinjiankang/article/details/89711493

【springboot全局异常处理】— 请求参数异常+自定义异常+其他异常

https://blog.csdn.net/nrsc272420199/article/details/102645938/

-------------Keep It Simple Stupid-------------
0%