为了方便理解和构造使用场景,目前假设存在三个实体对象,分别是ProjectDTO(项目)、TeamDTO(团队)和MemberDTO(成员),彼此的关系是,一个项目中存在一个团队,一个团队中存在多个成员,实体类里面的属性虚构,目的是为了举例校验的相关注解。
ProjectDTO(项目)实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ProjectDTO {
@NotBlank(message = "ID不能为空", groups = {TestValidGroup.Update.class})
private String id;
@NotBlank
@Pattern(regexp = "[a-zA-Z0-9]", message = "只允许输入数字和字母")
private String strValue;
@Min(value = -99, message = "值不能小于-99")
@Max(value = 100, message = "值不能超过100")
private Integer intValue;
@Negative(message = "值必须为负数")
private Integer negativeValue;
@EnumValue(strValues = {"agree", "refuse"})
private String strEnum;
@EnumValue(intValues = {1983, 1990, 2022})
private Integer intEnum;
@Valid
private TeamDTO teamDTO;
}
TeamDTO(团队)实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TeamDTO {
@FutureOrPresent(message = "只能输入当前年份或未来的年份")
private Year nowYear;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@Future(message = "只能是未来的时间")
private Date futureTime;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@Past(message = "只能是过去的时间")
private Date pastTime;
@Email(message = "请输入正确的邮箱")
private String email;
@Valid
private List<MemberDTO> list;
}
MemberDTO(成员)实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberDTO {
@NotBlank(message = "姓名不能为空")
private String name;
@EnumValue(intValues = {0, 1, 2}, message = "性别值非法,0:男,1:女,2:其他")
private Integer sex;
}
1. @Valid和@Validated 对应的maven包不一样
2. @Valid和@Validated中常用的注解
常用注解
| 注解 | 验证数据的类型 | 备注 |
|---|---|---|
| Null | 任意类型 | 参数值必须是 Null |
| NotNull | 任意类型 | 参数值必须不是 Null |
| NotBlank | 只能作用于字符串 | 字符串不能为 null,而且字符串长度必须大于0,至少包含一个非空字符串 |
| NotEmpty | CharSequence Collection Map Array | 参数值不能为null,且不能为空 (字符串长度必须大于0,空字符串(“ ”)可以通过校验) |
| Length | 字符串 | 限制字符串的大小 |
| Range | 数值 | 限制数值的范围 |
| Size | List | 限制列表的长度 |
| 任意类型 | 限制元素必须是电子邮箱 | |
| Future | 日期 | 限制必须是一个将来的日期 |
| Past | 日期 | 限制只能是过去的日期 |
| Pattern | 任意类型 | 限制必须符合指定的正则表达式 |
详细分析@NotNull和@NotBlank和NotEmpty如:
1.String name = null;则注解检查结果:
@NotNull: false
@NotEmpty:false
@NotBlank:false
2.String name = "";则注解检查结果:
@NotNull:true
@NotEmpty: false
@NotBlank: false
3.String name = " ";则注解检查结果:
@NotNull: true
@NotEmpty: true
@NotBlank: false
4.String name = "Great answer!";则注解检查结果:
@NotNull: true
@NotEmpty:true
@NotBlank:true
3. @Valid和@Validated区别和对应使用场景
| 类型 | Valid | Validated可以对参数校验进行分组 |
|---|---|---|
| 嵌套验证 | 可以 | 不可以 |
| 使用方式 | 用在方法/构造函数/方法参数和成员属性上 | 用在类/方法和方法参数上, 但不能用于成员属性 |
| 分组功能 | 没有 | 有 |
4. @Valid的嵌套校验(校验的对象中引入的其他对象或者List对象的校验)
@Valid
private List<MemberDTO> list;
5. @Validated的分组校验(不同的分组不同的校验策略)
例如: 更新项目信息, 项目id是必传项, 在新增项目时, 项目id可以不传, 新增和更新用的同一实体对象, 这个时候就需要根据不同的分组区分, 不同的分组采用不同的校验策略
@NotBlank(message = "ID不能为空", groups = {TestValidGroup.Update.class})
private String id;
group如何定义: 其实很简单, 就是自己定义一个接口, 这个接口的作用只是用来分组, 自己创建一个接口, 代码如下: 分别表示在新增和更新两种情况, 可以按实际需求在内部添加多个接口
public interface TestValidGroup {
interface Insert {
}
interface Update {
}
}
6. @Validated中的分组校验时@GroupSequence使用(指定字段的校验顺序)
如果不指定校验顺序, 每次校验的顺序不同, 错误提示信息也是不同的, 一些特定的场景要求固定错误顺序, 如: 自动化测试脚本, 每次都需要将返回校验的结果和预期结果比较, 返回的校验结果一直变化就会有问题.
controller层
@RestController
@RequestMapping("/valid")
public class TestValidController {
@PostMapping("/post")
public BaseResponse testValidPostRequest(@Validated(value = {TestValidGroup.Update.class}) @RequestBody ProjectDTO testAnnotationDto) {
return new BaseResponse(testAnnotationDto);
}
}
指定校验顺序用到@GroupSequence注解, 这个注解使用在group接口上, 可以针对每一个参数都进行分组, 然后通过该注解去指定顺序, 例如更新, 校验的顺序就是先校验group属于Id.class的字段, 再校验group属于StrValue的字段
public interface TestValidGroup {
@GroupSequence(value = {StrValue.class})
interface Insert {
}
@GroupSequence(value = {Id.class, StrValue.class})
interface Update {
}
interface Id {
}
interface StrValue {
}
}
注意: 此时不是校验group属于Update.class的字段, 而是校验group属于@GroupSequence的Value中的那些接口(id.class, StrValue.class)的字段, 如下: 正确用法:
@NotBlank(message = "ID不能为空", groups = {TestValidGroup.Id.class})
private String id;
错误用法:
@NotBlank(message = "ID不能为空", groups = {TestValidGroup.Update.class})
private String id;
7. 快速失败机制(单个参数校验失败后, 立马抛出异常, 不再对剩下的参数进行校验)
Validation提供了快速失败的机制
@Configuration
public class ValidConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失败模式
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
8. 自定义校验注解,实现特殊的校验逻辑
9. 全局异常处理, 统一返回校验异常信息
项目中一般会针对异常进行统一处理, valid校验失败的异常是MethodArgumentNotValidException, 所以可以拦截此类异常, 进行异常信息的处理, 捕获后的具体逻辑, 自行实现, 代码如下:
@Slf4j
@RestControllerAdvice
public class ExceptionHandlerConfig {
/**
* 拦截valid参数校验返回的异常,并转化成基本的返回样式
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public BaseResponse dealMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("this is controller MethodArgumentNotValidException,param valid failed", e);
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
String message = allErrors.stream().map(s -> s.getDefaultMessage()).collect(Collectors.joining(";"));
return BaseResponse.builder().code("-10").msg(message).build();
}
}
10. @Interface List的使用场景
同一字段在不同的场景下, 需要采用不同的校验规则, 并返回不同的异常信息, 目前有两种方式, 一种是采用@List的方式, 一种是在字段上重复使用同一个注解, 代码如下:
@Data
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
public class BaseDTO {
@NotBlank.List({
@NotBlank(message = "项目BaseId不能为空", groups = {TestValidGroup.Project.class}),
@NotBlank(message = "团队BaseId不能为空", groups = {TestValidGroup.Team.class})
})
private String baseId;
@Max(value = 10, message = "项目BaseId不能大于10", groups = {TestValidGroup.Project.class})
@Max(value = 30, message = "团队BaseId不能大于30", groups = {TestValidGroup.Team.class})
private Integer number;
}
目的是通过指定注解归属于不同的分组来到达区分的效果. controller如下:
@RestController
@RequestMapping("/valid")
public class TestValidController {
@PostMapping("/projectList")
public BaseResponse projectList(@Validated(value = {TestValidGroup.Project.class}) @RequestBody BaseDTO baseDTO) {
return new BaseResponse(baseDTO);
}
@PostMapping("/teamList")
public BaseResponse projectTeam(@Validated(value = {TestValidGroup.Team.class}) @RequestBody BaseDTO baseDTO) {
return new BaseResponse(baseDTO);
}
}
11. @Valid和@Validated组合使用
@Validated和Valid肯定是可以组合使用的,一种是分组,一种是嵌套,单独使用的注意点已经在上面的部分写过,下面简单描述下在Controller代码中的使用,其实很简单,就是在实体类(ProjectDTO)上加@Validated即可,内部的@Valid校验也会生效,代码如下:
@RestController
@RequestMapping("/valid")
public class TestValidController {
@PostMapping("/post")
public BaseResponse testValidPostRequest(@Validated(value = {TestValidGroup.Update.class, Default.class}) @RequestBody ProjectDTO testAnnotationDto) {
return new BaseResponse(testAnnotationDto);
}
}