导读
在 JavaEE 项目中, RestFull 层接收参数首先要对一些字段的格式进行校验,以防止所有查询都落到数据库,这也是一种合理的限流手段。以前基本上都是用 if…else…,这样的代码太啰嗦,除了使用策略模式进行优化,今天介绍一下校验注解@Valid,@Validated和@PathVariable,不仅可以减轻代码量,还加强了代码的易读性。
正文
1. @Valid 和 @Validated 区别
先讲一下这两个注解:@Valid与@Validated都是用来校验接收参数的,如果不使用注解校验参数,那么就需要在业务代码中逐一校验,这样会增加很多的工作量,并且代码不优美。
刚开始接触的时候多半会被弄混,实际上二者差距还是挺大的。根据自己的项目经验,@Validated和@Valid各有特点,可以联合使用。
- 提供者
javax.validation.Valid:使用 Hibernate validation 的时候使用,是 JSR-303 规范标准注解支持。如果你是 springboot 项目,那么可以不用单独引入依赖了,因为它就存在于最核心的 web 开发包(spring-boot-starter-web)里面;
org.springframework.validation.annotation.Validated:只用 Spring Validator 校验机制使用,是 Spring 做得一个自定义注解,增强了分组功能;
- 标注位置
@Validated:可以用在类型、方法和方法参数上,不能用于成员属性(field)上。如果注解在成员属性上,则会报不适用于field的错误;
@Valid:可以用在方法、构造函数、方法参数和成员属性(field)上;
- 分组支持
@Validated:提供分组功能,可以在参数验证时,根据不同的分组采用不同的验证机制;
@Valid:没有分组的功能,不能进行分组校验;
-
嵌套支持
@Validated:不能进行嵌套对象校验;
@Valid:可以进行嵌套校验,但是,需要在嵌套的字段上面加上注解;
2. 常用的校验方法
- Debug进入jar包,可以看到全量的相关注解:
- 简述一些常用注解:
注解 | 使用方法 |
---|---|
@AssertFalse | 被校验的对象必须为 true。
|
@AssertTrue | 被校验的对象必须为 false。 |
@DecimalMax(value = “val”) | 被校验的对象必须是数字,而且小于等于 val。
|
@DecimalMin(value = “val”) | 被校验的对象必须是数字,而且大于等于 val。
|
@Digits(integer = in, fraction = fra) | 校验字符串是否是符合指定格式的数字:in 指定整数精度,fra 指定小数精度。 |
@Future | 被校验的对象(日期类型)必须是将来时间,即:比当前时间晚。 |
@Past | :被校验的对象(日期类型)必须是过去时间,即:比当前时间早。 |
@Size(min = min, max = max) | 元素值的在 min 和 max(包含)指定区间之内,如字符长度、集合大小(对于集合来说,null 和空字符串都是算长度的)。 |
@NotBlank |
所注解的元素不能为null且不能为空白,并且必须至少包含一个非空白字符,用于校验CharSequence(含String、StringBuilder和StringBuffer)。只支持字符类型。 |
@NotEmpty |
所注解的元素不能为null且长度大于0,可以是空白,用于校验 CharSequence、数组、Collection 和 Map。 |
@NotNull |
所注解的元素不能为 null,接受任何类型。 |
@Null |
所注解的元素必须为 null,接受任何类型。 |
@Pattern(regexp = “正则表达式”, message = “”) |
所注解的元素必须匹配指定的正则表达式。 注意:如果 @Pattern 所注解的元素是null,则@Pattern 注解会返回 true,即也会通过校验,所以应该把 @Pattern 注解和 @NotNull 注解结合使用。 |
3. @Validated分组校验
场景:多个 Restfull 接口共用一个标准 Bean,每个接口的参数相同,但是需要校验的参数(必输项)却不完全相同,这样的场景可以使用 @Validated,因为它提供了分组校验的功能。
分组 | 说明 |
---|---|
隐式分组 |
1.没有显式分组的默认都是 Default 组; 2.显式分组之后,剩下的那些没有被划分到自建组的字段都属于 Default 组; 3.平常我们写 |
显式分组 |
1.自定义interface接口的分组,属于自建组; 2.自建组可以继承 Default.class,也可以不继承 Default.class,两者意义不同; 3.多个分组可以一起实用; 4.分组机制让我们可以很灵活的使用对象里面的某些字段,以实现高权限等级参数传递校验等操作。 |
-
新建请求对象
@Data
public class TeacherDTO {
@NotBlank(message = "id必传")
private String id;
@NotBlank(message = "不能没有名称")
private String name;
@NotNull(message = "age必传")
private Integer age;
@NotBlank(message = "不能没有idCard")
private String idCard;
@NotBlank(message = "老师不能没有手机号", groups = OnlyTeacher.class)
private String phone;
@NotEmpty(message = "学生不能没有书")
@Size(min = 2, message = "学生必须有两本书", groups = OnlyStudent.class)
private List bookNames;
@NotEmpty
@Size(min = 1, message = "老师不能没有学生", groups = TeacherWithDefault.class)
private List studentList;
}
-
新建分组
// Teacher分组
public interface TeacherValid { }
// Student分组
public interface StudentValid { }
// 继承Default的分组
public interface OtherValid extends Default{ }
-
接口测试
/**
* Created by tjm on 2022/11/11.
*/
@RestController
@RequestMapping("/test")
public class TestValidController {
private static final Logger LOGGER = LoggerFactory.getLogger(TestValidController.class);
/**
* 测试 - 分组校验 - 默认default
*/
@PostMapping("/only/default")
public Object testDefaultValid(@Validated TeacherDTO param, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
}
return ResultGenerator.genSuccessResult();
}
/**
* 测试 - 分组校验 - 只有teacher
*/
@PostMapping("/only/teacher")
public Object testOnlyTeacherValid(@Validated(OnlyTeacher.class) TeacherDTO param, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
}
return ResultGenerator.genSuccessResult();
}
/**
* 测试 - 分组校验 - 只有student
*/
@PostMapping("/only/student")
public Object testOnlyStudentValid(@Validated(OnlyStudent.class) TeacherDTO param, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
}
return ResultGenerator.genSuccessResult();
}
/**
* 测试 - 分组校验 - teacher + default
*/
@PostMapping("/with/teacher")
public Object testWithTeacherValid(@Validated({OnlyTeacher.class, Default.class}) TeacherDTO param, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
}
return ResultGenerator.genSuccessResult();
}
/**
* 测试 - 分组校验 - 继承default
*/
@PostMapping("/with/default")
public Object testWithDefaultValid(@Validated(TeacherWithDefault.class) TeacherDTO param, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
}
return ResultGenerator.genSuccessResult();
}
}
-
结果
分组 | 校验参数 |
---|---|
只有 default | id、name、age、idCard |
只有 teacher | phone |
只有 student | booknames |
teacher + default | id、name、age、idCard、phone |
teacher 继承 default | id、name、age、idCard、studentList |
4.@Valid嵌套校验
- 新建请求对象
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
// 嵌套验证必须用 @Valid
@Valid
@NotNull(message = "props不能为空")
@Size(min = 1, message = "props至少要有一个自定义属性")
private List props;
}
public class Prop {
@NotNull(message = "pid不能为空")
@Min(value = 1, message = "pid必须为正整数")
private Long pid;
@NotNull(message = "vid不能为空")
@Min(value = 1, message = "vid必须为正整数")
private Long vid;
@NotBlank(message = "pidName不能为空")
private String pidName;
@NotBlank(message = "vidName不能为空")
private String vidName;
}
- 接口测试
/**
* 测试 - 分组校验 - 继承default
*/
@PostMapping("/item")
public Object testItemValid(@Validated Item param, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
}
return ResultGenerator.genSuccessResult();
}
- 测试结果
1. 不仅校验 Item 参数,还会校验子类 Prop 参数;
2. 注意:嵌套验证必须在子参数上用 @Valid。
5.Restfull层@Validated的使用
校验参数的时候,如何判断并返回失败的结果?一般有两种方式:
- 全局异常捕获
@ControllerAdvice
@RestController
@Slf4j
public class GlobalExceptionHandler {
/**
* 非法参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(value = HttpStatus.OK)
public ApiResult handleMethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
List list = new ArrayList();
List fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
list.add(fieldError.getDefaultMessage());
}
Collections.sort(list);
log.error("fieldErrors" + JSON.toJSONString(list));
return ApiResult.fail(ApiCode.PARAMETER_EXCEPTION, list);
}
}
- 用 BindingResult 在实体类校验信息返回结果绑定
即使是全局异常捕获的方式,也能看到:校验信息是被封装在 BindingResult 对象里的,所以,我们也可以在 RestFull 层直接取。
1. BindingResult用在实体类校验信息返回结果绑定;
2. BindingResult.hasErrors()判断是否校验通过,bindingResult.getFieldError().getDefaultMessage() 获取在 TestEntity 的属性设置的自定义message,如果没有设置,则返回默认值 “javax.validation.constraints.XXX.message”。
可以看到,我上面的例子用的都是这种方法,我觉得这样更方便、直观,维护性更好。