日常搬砖中经常遇到需要对接口方法进行校验,除了常规的 if 条件判断,是否还有更加优雅的处理方法?我们经常从公众号或者其他博文上看到的可能就是 spring-boot-starter-validation 提供的方法,今天主要对这个操作进行详细讲解,提供可以实操的方案。
使用流程
1、需要的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2、在需要校验的实体类的字段上添加相应的校验注解。
public class User {
@NotBlank(message = "用户名不能为空")
private String username;
@Size(min = 6, max = 12, message = "密码长度必须在6-12个字符之间")
private String password;
// getters and setters
}
3、在需要校验参数的Controller类或方法上添加@Validated
注解。
@RestController
@Validated
public class UserController {
@PostMapping("/users")
public ResponseEntity createUser(@Valid @RequestBody User user) {
// 校验通过,执行业务逻辑
// ...
return ResponseEntity.ok().build();
}
}
通常我们一般看到的信息可能就到这里就完了,实际上要做的还有很多。因为上面的接口返回信息如下:
{
"timestamp": "2024-03-08T01:42:23.301+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.user.username",
"NotBlank.username",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"user.username",
"username"
],
"arguments": null,
"defaultMessage": "username",
"code": "username"
}
],
"defaultMessage": "用户名不能为空",
"objectName": "user",
"field": "username",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='user'. Error count: 1",
"path": "/users"
}
这个结果我们能接受吗? 不应该返回一个简单的提示吗? 返回这一大串不是我们想要的。所以是需要进行额外的处理
短路校验
很多时候我们要校验多个字段时,并不需要一个一个全部校验完毕了再返回错误信息,而是一单发现有一个入参不对,就立马返回错误,不再对剩余的参数进行相关校验,我们需要进行相关配置:
@Configuration
public class ValidConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失败模式
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
统一异常处理
校验失败后,默认的接口返回含有太多的信息,不符合我么统一包装的返回处理,需要进行处理。一般,校验失败产生 org.springframework.web.bind.MethodArgumentNotValidException 异常,需要对这个进行拦截处理
@RestControllerAdvice
public class ExceptionHandlerConfig {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity dealMethodArgumentNotValidException(MethodArgumentNotValidException e) {
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
String message = allErrors.stream().map(s -> s.getDefaultMessage()).collect(Collectors.joining(";"));
final ResponseEntity<String> response = ResponseEntity.status(400).body(message);
return response;
}
}
BindingResult 使用
除了统一异常处理的方法外,我们也可以搭配 bindingResult 这个类一起使用
@PostMapping("/est")
public MyResponse test(@RequestBody @Valid TestEntity test,BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
retrun new MyResponse("错误提示码",bindingResult.getFieldError().getDefaultMessage());
}
}
@Validated 和 @Valid
上面的代码涉及了这2个注解,我们来学习一下:
@Validated
是 Spring 框架提供的注解,用于标注在类、方法、参数上,但是不能用在字段上,提供了一个分组功能。
@Valid
是 Java 标准库(javax.validation)中的注解,用于标注在类的属性或方法参数上,它是 JSR-303(Bean Validation)的一部分,是一种基于注解的校验规范,可以进行级联和递归校验。
有了上面的了解,在日常coding中我们要避免一些失效的场景:
1、参数如果是非对象格式,需要在controller类上面添加@Validated注解
2、参数如果是对象的话,属性的前面的需要添加 @Valid 或 @Validated 注解
3、如果是嵌套对象的话,里面的对象还要添加 @Valid注解
常见的一些参数校验注解主要有:
@Null 限制只能为null
@NotNull 限制必须不为null
@AssertFalse 限制必须为false
@AssertTrue 限制必须为true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Future 限制必须是一个将来的日期
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@Past 限制必须是一个过去的日期
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在min到max之间
@Past 验证注解的元素值(日期类型)比当前时间早
@NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,
@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
@Range(min=, max=,message=) 检查数字是否介于min和max之间.
@Validated的分组校验
同一个实体类在不同接口下可能都会做作为参数,而同一个字段在不同业务场景下的校验规则又可能不一样,这里就需要我们通过分组来解决这样的问题
分组接口
public interface TestGroup {
interface Test1{
}
interface Test2{
}
}
实体类的分组规则
@Data
public class QueryDTO {
@NotBlank.List({
@NotBlank(message = "id不能为空", groups = {TestGroup.Test1.class}),
@NotBlank(message = "id不能为空", groups = {TestGroup.Test2.class})
})
private Long id;
@Max(value = 10, message = "age不能大于10", groups = {TestGroup.Test1.class})
@Max(value = 30, message = "age不能大于30", groups = {TestGroup.Test2.class})
private Integer age;
}
入参要指定分组
@PostMapping("/test2")
public ResponseEntity test2(@Validated(value = {TestGroup.Test1.class}) @RequestBody QueryDTO queryDTO) {
return ResponseEntity.ok().build();
}