本文主要介绍SpringBoot中如何使用@Validated,@Valid及其相关注解,以及全局异常捕获对接口入参进行优雅校验和返回自定义异常返回客户端。
接口实体类如下
@Data
public class User implements Serializable {
@NotBlank(message = "姓名不能为空", groups = {ValidateType.SELECT.class})
private String name;
@Pattern(regexp = "0?(13|14|15|17|18|19)[0-9]{9}", message = "手机号格式不正确")
private String phone;
@Positive(message = "要为正数")
@Max(value = 99, message = "不能超过99", groups = ValidateType.SELECT.class)
private Integer age;
@NotNull(message = "token不能为null", groups = ValidateType.INSERT.class)
private String token;
@NotBlank(message = "openUid不能为blank")
@Length(min = 5, max = 10, message = "范围为5--10之间")
private String openUid;
@NotEmpty(message = "address不能为empty")
private String address;
@Email(message = "邮箱格式错误",regexp = "^[A-Za-z0-9\\u4e00-\\u9fa5]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$")
@NotBlank(message = "邮箱不能为空")
private String email;
@Future(message = "日期必须是将来某个时候", groups = ValidateType.class)
@NotNull(message = "日期不能为空", groups = ValidateType.class)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss",iso = DateTimeFormat.ISO.DATE_TIME)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalTime createTime;
@JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "HH:mm:ss")
private LocalTime updateTime;
@Valid
@NotNull(message = "userList不能为空")
private List<UserVO> userVOList;
@Valid
@NotNull(message = "userVo不能为空")
private UserVO userVO;
}
@Data
public class UserVO {
@NotNull(message = "姓名不能为空")
@Length(min = 5,max = 10)
private String name;
@Positive
@Max(value = 100,message = "你真的100岁了吗?")
@Min(value = 0,message = "怎么会有负数岁数的人呢?")
private Integer age;
@Pattern(regexp = "0?(13|14|15|17|18|19)[0-9]{9}", message = "userVO手机号格式不正确")
private String phoneNumber;
@NotBlank(message = "地址不能为空")
private String address;
// @Email(message = "邮箱格式错误")
@Pattern(regexp = "^[A-Za-z0-9\\u4e00-\\u9fa5]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$",message = "userVO邮箱正则校验失败")
@NotBlank(message = "vo邮箱不能为空")
private String email;
@PastOrPresent(message = "日期为过去或者现在")
@NotNull(message = "日期不能为空")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
// @JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd" ,timezone = "GMT+8")
private LocalDateTime createTime;
}
以上为两个实体类。在user中嵌套了userVo,也可以进行嵌套验证。
注解 | 解释 |
---|---|
@Null | 限制只能为null |
@NotEmpty | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@NotBlank | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
@NotNull | 限制必须不为null |
@AssertFalse | 限制必须为false |
@AssertTrue | 限制必须为true |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
@Future | 限制必须是一个将来的日期 |
@Past | 限制必须是一个过去的日期 |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Size(max,min) | 限制字符长度必须在min到max之间 |
验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 |
以上表格包括了常用的一些字段校验。全部的注解在以下位置
一、分组校验
- 配置分组接口类
在这里定义了增删改查四种业务类型
public interface ValidateType {
public interface INSERT {};
public interface SELECT {};
public interface UPDATE {};
public interface DELETE {};
}
- 在需要分组校验的字段上加上对应的分组接口
例如要在查询接口上校验姓名字段,并且不能为空;可以如下标注字段,groups表示我想在查询的接口上对name进行校验,
public class User implements Serializable {
@NotBlank(message = "姓名不能为空", groups = {ValidateType.SELECT.class})
private String name;
}
- 控制器对应的接口配置,在请求体中加上@Validated(ValidateType.SELECT.class),表示这个接口入参要对标记了这个分组的字段进行相应的校验,不会校验其他类型。
@PostMapping(value = "/select")
public Result findUser(@RequestBody @Validated(ValidateType.SELECT.class) User user){
try {
return ResultUtil.successData(user);
} catch (Exception e) {
return ResultUtil.failureMsg(ResultEnum.SQL_FAILURE.getMsg());
}
}
- 测试一下
可以看到在select接口中 ,如果没有name字段,或者字段值为空字符串,或者为空格,都会触发我们配置的校验规则,返回对应的message。
可以在其他接口中再测试一下。我们对name字段配置的是 ValidateType.SELECT.class 类型。我们在更新接口中测试一下
@PostMapping(value = "/update")
public Result update(@RequestBody @Validated(ValidateType.UPDATE.class) User user) {
try {
return ResultUtil.successData(user);
} catch (Exception e) {
return ResultUtil.failureMsg(ResultEnum.SQL_FAILURE.getMsg());
}
}
更新接口中我们配置的是 @Validated(ValidateType.UPDATE.class) 这种校验规则。此时name字段就会被忽略校验,因为name字段是ValidateType.SELECT.class 类型的规则。验证一下
可以看到请求体中没有name,依旧请求成功
加上name字段,但是为空格也请求成功,如果是空字符串依旧成功。这里不做演示了。
通过例子以上可对其他字段做对应的校验,并且加入对应的校验分组,可以灵活的控制不同接口不同字段的校验规则。
二、默认校验分组
框架为我们配置了默认的分组,如果我们不手动配置分组,则会进行默认分组校验。openUid字段没有加入任何一个分组,下面我们对上述的openUid字段进行验证一下。
我们 首先新建一个all接口
@PostMapping(value = "/all")
public Result all(@RequestBody @Validated User user) {
try {
return ResultUtil.successData(user);
} catch (Exception e) {
return ResultUtil.failureMsg(ResultEnum.SQL_FAILURE.getMsg());
}
}
这个接口中@Validated 注解没有任何分组。我们加上一个有效的openUid字段请求一下,看会发生什么
可以看到请求成功并且返回了请求体。
去掉该字段就会报错
如果是个空字符串或者空格,依旧报错
这里返回了:范围在5–10之间,这是因为我们在上面配置了@length 注解,范围最小是5,最大是10。
如果将接口中的验证分组加上,结果将会怎么样呢?
@PostMapping(value = "/all")
public Result all(@RequestBody @Validated(ValidateType.class) User user) {
System.out.println(user);
int i = 0;
try {
return ResultUtil.successData(user);
} catch (Exception e) {
return ResultUtil.failureMsg(ResultEnum.SQL_FAILURE.getMsg());
}
}
这里加上了ValidateType.class 这个分组,openUid上没有任何分组,验证一下
可以看到没有openUid字段,是请求成功的。
从以上例子可以得出,分组配置是严格控制的,每个分组只会校验自己分组下的字段。
就算是默认分组也不会对其他分组造成干扰,这样就对字段进行了严格的分组和校验。
三、嵌套校验
一般项目中会根据业务需要,要定义一些嵌套对象,比如人这个实体会包括以下属性,姓名、性别、等等,也会有所有房屋、儿女、汽车等等实体。其中属性可以是单个对象,也可以是对象的集合List,这种场景就可以使用嵌套校验。
上述我们定义了另外一个userVO,并且在user中将其注入为一个userVO和一个userVO的List集合,要进行嵌套校验则可以进行如下配置
@Valid
@NotNull(message = "userList不能为空")
private List<UserVO> userVOList;
@Valid
@NotNull(message = "userVo不能为空")
private UserVO userVO;
在嵌套对象和list上加上@Valid。表示要对这个对象进行嵌套校验。在userVO中每个字段可以有对应的自己的校验规则。
配置好后我们对userVO的phoneNumber字段进行验证,手机号我们配置的是正则表达式,可以对其进行验证一下。
@Pattern(regexp = "0?(13|14|15|17|18|19)[0-9]{9}", message = "手机号格式不正确")
private String phoneNumber;
接口类
@PostMapping(value = "/recursive")
public Result recursive(@RequestBody(required = true) @Validated User user) {
try {
return ResultUtil.successData(user);
} catch (Exception e) {
return ResultUtil.failureMsg(ResultEnum.SQL_FAILURE.getMsg());
}
}
可以看到请求成功,改变一下phoneNumber的值,就会报错
如果请求参数中没有userVO,就会报错
1、@Valid和@NotNull的组合使用
上面我们在userVO字段上使用了@Valid、@NotNull(message = “userVO不能为空”)两个组合注解,
如果我们只加其中一个会怎么样呢?验证一下
@Valid
private UserVO userVO;
1.1只加上@Valid 注解
我们先只加上@Valid 注解
我们全部字段都符合校验规则,请求成功
1.1.1 不加userVO对象
接下来我们在请求中不加userVO。结果成功
1.1.2 加上正确的userVO对象
请求成功
1.1.3 加上错误的userVO对象
加上userVO,但是手机号字段输入错误的,结果触发了校验规则
1.2 只加@NotNull 注解
@NotNull(message = "userVo不能为空")
private UserVO userVO;
1.2.1 不加userVO对象
触发了@NotNull 校验规则
1.2.2 加上正确的userVO对象
请求成功
1.2.3 加上错误的userVO对象
依旧成功。并没有触发校验规则
四、全局异常捕获
1、配置全局异常捕获类
@Slf4j
@RestControllerAdvice
public class GlobalException {
/**
* 校验URL?param=value,参数key是否缺失
* @param e 忽略参数异常
* @return Result
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingServletRequestParameterException.class)
public Result parameterMissingHandler(MissingServletRequestParameterException e) {
return ResultUtil.failureMsg("请求参数 " + e.getParameterName() + " 不能为空");
}
/*
* @Author: zhanggeyang
* @Description:校验URL?param=value,参数value是否缺失
* @Date: 4:09 下午 5/13/23
* @Param: [e]
* @Return: com.example.mybatisplustest.entity.Result
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({ConstraintViolationException.class})
public Result otherExceptionHandler(ConstraintViolationException e) {
// 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
if (!StringUtils.isEmpty(e.getMessage())) {
return ResultUtil.failureMsg(e.getMessage());
}
return ResultUtil.failure(ResultEnum.SERVICE_FAILURE);
}
/**
* 缺少请求体异常处理器
* @param e 缺少请求体异常
* @return Result
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public Result parameterBodyMissingExceptionHandler(HttpMessageNotReadableException e) {
// log.info("出错了:{}", e.getLocalizedMessage());
Throwable rootCause = e.getRootCause();
//校验参数是否多余,导致不能识别
if (rootCause instanceof JsonProcessingException) {
JsonMappingException mappingException = (JsonMappingException) rootCause;
String fieldName = mappingException.getPath().get(0).getFieldName();
return ResultUtil.failureMsg("参数 " + fieldName + " 不能识别");
}
//校验日期字段转换是否异常
if (rootCause instanceof DateTimeException) {
return ResultUtil.failureMsg("日期转换异常");
}
//如果都不属于,则属于请求体缺失
return ResultUtil.failureMsg("请求体缺失");
}
/**
* 嵌套对象字段缺失会走这里
* @param e 参数验证异常
* @return ResponseInfo
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result parameterExceptionHandler(MethodArgumentNotValidException e) {
// log.error("", e);
// 获取异常信息
BindingResult exceptions = e.getBindingResult();
// 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
if (exceptions.hasErrors()) {
List<ObjectError> errors = exceptions.getAllErrors();
if (!errors.isEmpty()) {
// 这里列出了全部错误参数,按正常逻辑,只需要第一条错误即可
FieldError fieldError = (FieldError) errors.get(0);
return ResultUtil.failureMsg(fieldError.getDefaultMessage());
}
}
return ResultUtil.failure(ResultEnum.SERVICE_FAILURE);
}
/*
* @Author: zhanggeyang
* @Description: 捕获请求方法异常,比如post接口使用了get
* @Date: 8:20 下午 5/13/23
* @Param: [e]
* @Return: com.example.mybatisplustest.entity.Result
*/
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result methodNotAllowedHandler(HttpRequestMethodNotSupportedException e) {
String method = e.getMethod();
return ResultUtil.failureMsg(method + "请求方法不被允许");
}
}
上述配置基本涵盖了我们校验中的常用异常
比如我们输入的时间格式和配置的yyyy-MM-dd HH:mm:ss不一样,就会抛出日期转换异常
比如这个接口,两个参数可以进行校验分为key不存在和value不存在两种情况
@GetMapping(value = "/p")
public Result p(@RequestParam(value = "p") @NotBlank(message = "p不能为空") String p, @RequestParam(value = "n") @NotBlank(message = "n不能为空") String string) {
try {
return ResultUtil.successMsg(p + "==" + string);
} catch (Exception e) {
return ResultUtil.failureMsg(ResultEnum.SQL_FAILURE.getMsg());
}
}
五、总结
1、分组校验
需要在要校验的字段上加上校验规则和对应的分组。并且每个分组之间是互相隔离的,不会影响其他的分组。没有分组的就会被加入框架默认分组。
2、嵌套校验
嵌套校验可以加载对应的对象和对象集合上。需要@Valid和@NotNull注解配合使用。
2.1 只有@Valid
请求中不加对应的对象,或者加上正确的对象,都会请求成功。
加上错误的对象,就会触发具体的字段校验规则。
2.2 只有@NotNull
不加对应的对象只会触发非空校验
加上正确或者错误的对象,都会请求成功,不会触发具体的字段校验规则。
综上所述,在嵌套校验中要成功校验,需要2个注解同时使用,才能保证非空判断和具体的字段校验都成功。
结语
以上就是字段校验结合全局异常捕获的使用。