在项目中使用Spring 自带的 @Validated、@Valid 等校验注解来替代手动对参数进行校验(if…else一堆),本文涉及这类校验注解的基本用法、高阶玩法及衍生用法,也包括知识拓展等
1、概念:
@Valid是JSR303声明的,其中JSR303是JAVA EE 6 中的一项子规范,叫做 Bean Validation,为 JavaBean 验证定义了相应的元数据模型和 AP,Hibernate validation对其进行实现;
2、项目引入方式:所属包为:javax.validation.Valid
相关pom依赖:
org.hibernate.validator
hibernate-validator
。。。。。。
3、作用范围:
@Valid:作用在方法、构造函数、方法参数和成员属性(field)上
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Valid {
}
1、概念:
Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种),是一套帮助我们继续对传输的参数进行数据校验的注解,通过配置Validation可以很轻松的完成对数据的约束。
2、项目引入方式:
相关pom依赖:
org.springframework.boot spring-boot-starter-web 3、作用范围:
@Validated作用在类、方法和参数上;
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
Class>[] value() default {};
}
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:如下
1、注解位置:
@Validated:用在类型、方法和方法参数上。但不能用于成员属性(field)。
@Valid:可以用在方法、构造函数、方法参数和成员属性(field)上。
2、分组:
@Validated:提供分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。
@Valid:作为标准JSR-303规范,没有分组功能。
3. 嵌套验证:
一个待验证的pojo类,其中还包含了待验证的对象,需要在待验证对象上注解@valid,才能验证待验证对象中的成员属性。
@Validated: 用在方法入参上无法单独提供嵌套验证功能,不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
@Valid: 用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
@Target 设定注解的不同使用范围;
@Target 共以下八种使用范围:
@Target:注解的作用目标
@Target(ElementType.TYPE) //接口、类、枚举
@Target(ElementType.FIELD) //字段、枚举的常量
@Target(ElementType.METHOD) //方法
@Target(ElementType.PARAMETER) //方法参数
@Target(ElementType.CONSTRUCTOR) //构造函数
@Target(ElementType.LOCAL_VARIABLE)//局部变量
@Target(ElementType.ANNOTATION_TYPE)//注解
@Target(ElementType.PACKAGE) ///包
注解 | 解析 | null是否能通过验证 |
@AssertFalse | 该字段值为false时,验证才能通过 | YES |
@AssertTrue | 该字段值为true时,验证才能通过 | YES |
@DecimalMax | 验证小数的最大值 |
YES |
@DecimalMin | 验证小数的最小值 | YES |
@Digits | 验证数字的整数位和小数位的位数是否超过指定的长度 |
YES |
该字段为Email格式,才能通过 | YES | |
@Future | 验证日期是否在当前时间之后,否则无法通过校验 |
YES |
@FutureOrPresent | 时间在当前时间之后 或者等于此时 | |
@Max | |
YES |
@Min | 同上,不能低于某个值否则无法通过验证 | YES |
@Negative | 数字<0 | YES |
@NegativeOrZero | 数字=<0 | YES |
@NotBlank | 该注解用来判断字符串或者字符,只用在String上面 | NO |
@NotEmpty | 不能是null 不能是空字符 集合框架中的元素不能为空 |
NO |
@NotNull | 使用该注解的字段的值不能为null,否则验证无法通过 | NO |
@Null | 修饰的字段在验证时必须是null,否则验证无法通过 | YES |
@Past | 验证日期是否在当前时间之前,否则无法通过校验 | YES |
@PastOrPresent | 用于验证字段是否与给定的正则相匹配 | YES |
@Pattern | |
YES |
@Positive | 数字>0 | YES |
@PositiveOrZero | 数字>=0 | YES |
@Size | |
YES |
经常出现在实体类中,比如@NotNull、@NotBlank、@NotEmpty等
前端传过来的字段如何通过某些注解在后台做校验?最笨的方法就是通过if else判断校验,非常笨重、不灵活。如果前端传来100个字段你还用if else 做判断吗?不得累死 接下来的 第一个场景 演示的就是在后台创建的实体和前端传来的字段做对应映射,加上JSR303注解来做灵活校验,减轻代码量。
具体示列 entity类:
@ApiModelProperty(value = "id自增列")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@NotBlank(message = "用户名称不能为空")
@ApiModelProperty(value = "用户名称")
private String userName;
@Min(value = 1,message = "输入年龄不能小于1")
@Max(value = 150,message = "输入年龄不能超过150")
@NotNull(message = "输入年龄不能为空")
@ApiModelProperty(value = "密码")
private Integer userAge;
@NotBlank(message = "密码不能为空")
@Length(min = 6, max = 10,message = "密码⻓度⾄少6位但不超过10位!", groups = {UpdateGroup.class, AddGroup.class})
@ApiModelProperty(value = "密码")
private String userPwd;
@Email(message = "邮箱格式不正确")
@ApiModelProperty(value = "邮箱")
private String email;
其实@Validated是在@Valid上又封装了一层,比如加上了分组功能,这个分组详细说,目前就先不透露啦;
使用效果:校验错误以后会有默认的响应,在给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果;
示例 Controller:
@PostMapping(value = "/saveUser")
@ApiOperation(value = "用户添加", notes = "用户添加")
public ResultResponse saveUser(@Validated(AddGroup.class) UserEntity userEntity, BindingResult bindingResult) {
/**
* result.hasErrors()如果前端传来的值和在实体类标注的注解不对应就返回false。
* 比如在实体在name字段标注了@NotBlank(message = "用户名称不能为空")而前端没传name就返回了false;
*/
if (bindingResult.hasErrors()) {
Map map = new HashMap<>();
//1、result.getFieldErrors()获取校验的错误结果对象然后遍历
for (FieldError fieldError : bindingResult.getFieldErrors()) {
//FieldError 获取到错误提示
String message = fieldError.getDefaultMessage();
//获取错误的属性的名字
String field = fieldError.getField();
map.put(field, message);
}
return new ResultResponse<>(map);
}
return new ResultResponse<>(testService.save(userEntity));
}
效果演示1:请求用户新增接口,什么参数都不传,检验是否能有返回参数校验错误信息
效果演示2:请求用户新增接口,传密码参数,且传的值故意令长度不符合验证要求,别的参数就不一 一测试校验了,都是一样的用法,只要是不符合参数校验都会返回你所设定的message内容:
@Length(min = 6, max = 10,message = “密码⻓度⾄少6位但不超过10位!”)
解读:表示该userPwd 密码长度设定在6-10之间,密码长度低于6位或者高于10位都会有message提示。
效果演示3:请求用户新增接口,测试@Email 注解使用,检验是否能有返回参数校验错误信息。
效果演示4:请求用户新增接口,全部参数都按要求传,检验是否能新增成功。
结合一场景,我们已经知道了如何校验参数并捕获错误信息,然后返回给前端, 如果按照这样的写法,一百个接口,就得在每个方法都写判断和捕获参数消息遍历的代码,这肯定是不行,冗余代码太多,难以维护
可能想到了全局异常捕获,全局异常捕获完全可以做。但是今天我不这么玩,我教大家一个别致的玩法,比如aop,什么意思呢,就是自定义一个异常切面去捕获每次所抛出的异常信息,由于aop是先于全局异常执行顺序,只需在切片中返回具体参数检验结果即可,其实跟定义全局异常捕获一个道理;
@Order(10)
@Around("execution(public * .controller.*.*(..))")
public ResultResponse validAspect1(ProceedingJoinPoint pjp) throws Throwable {
BindingResult bindingResult = getBindingResult(pjp);
//表示正常返回
if (bindingResult == null || !bindingResult.hasErrors()) {
ResultResponse resultResponse = new ResultResponse();
ResultResponse o = (ResultResponse) pjp.proceed(pjp.getArgs());
resultResponse.setData(o.getData());
return resultResponse;
} else {
//捕捉异常,将异常封装到map返回
Map map = new HashMap<>();
List errors = bindingResult.getFieldErrors();
errors.forEach(e -> map.put(e.getField(),e.getDefaultMessage()));
return new ResultResponse(map);
}
}
/**
* 获取接口中的BindingResult对象
*
* @param pjp 切入点
*/
private BindingResult getBindingResult(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
for (Object arg : args) {
if (arg instanceof BindingResult) {
return (BindingResult) arg;
}
}
return null;
}
@Order(10)
@Around(“execution(public * com.system.xiaoma.controller..(…))”)
public ResultResponse validAspect1(ProceedingJoinPoint pjp) throws Throwable {
BindingResult bindingResult = getBindingResult(pjp);
//表示正常返回
if (bindingResult == null || !bindingResult.hasErrors()) {
ResultResponse resultResponse = new ResultResponse();
ResultResponse o = (ResultResponse) pjp.proceed(pjp.getArgs());
resultResponse.setData(o.getData());
return resultResponse;
} else {
//捕捉异常,将异常封装到map返回
Map
List errors = bindingResult.getFieldErrors();
errors.forEach(e -> map.put(e.getField(),e.getDefaultMessage()));
return new ResultResponse(map);
}
}
/**
* 获取接口中的BindingResult对象
*
* @param pjp 切入点
*/
private BindingResult getBindingResult(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
for (Object arg : args) {
if (arg instanceof BindingResult) {
return (BindingResult) arg;
}
}
return null;
}
以上两个场景我们已经解决了如何统一处理前端传来的字段校验和如何统一处理返回校验错误消息提示。
接下再来假设一个场景: 比如我们添加一个用户存在userName,userAge,userPwd等些字段且必填,然后在实体类都加上了校验注解。
马上,我们又有一个 只修改用户名称的接口。只根据用户id修改userName。前端只传你用户id和新改的用户名userName,可我们在实体先前就标注了很多校验注解,除了id userName之外,别的参数不传肯定会验空返回,那单独给开个接口来做重命名?或者单独封装个只接id跟userName的实体类?
不不不,肯定是能复用就复用!
接下来我就来聊聊这个怎么解决这个问题吧,其实还是用@Validated,它有做分组功能。怎么理解呢?
意思是在新增接口我们给新增标识的校验注解, 在修改接口使用修改标识的校验注解,这样就不会新增修改接口冲突(分组校验(多场景的复杂校验))。如下:
@NotNull(message = "用户id不能为空", groups = UpdateGroup.class)
private Integer id;
这个UpdateGroup.class是干什么用的,其实就是我们自定义的一个标识用来告诉程序不同的接口校验不同的属性罢了;然后在新增接口上也同样加上 AddGroup.class 的分组标志,即可;
@PostMapping(value = "/update")
@ApiOperation(value = "用户修改", notes = "根据userId对用户信息修改")
public ResultResponse updateUser(@Validated(UpdateGroup.class) UserEntity userEntity, BindingResult bindingResult) {
// if (bindingResult.hasErrors()) {
// Map map = new HashMap<>();
// //1、result.getFieldErrors()获取校验的错误结果对象然后遍历
// for (FieldError fieldError : bindingResult.getFieldErrors()) {
// //FieldError 获取到错误提示
// String message = fieldError.getDefaultMessage();
// //获取错误的属性的名字
// String field = fieldError.getField();
// map.put(field, message);
// }
// return new ResultResponse<>(map);
// }
return new ResultResponse<>(testService.updateById(userEntity));
}
如下定义分组标识:
//新增标识
public @interface AddGroup {
}
//修改标识
public @interface UpdateGroup {
}
//查询标识
public @interface QueryGroup {
}
有朋友可能就会问了,要是既UpdateGroup标识,也AddGroup 分组标识的,怎么理解?
像这种情景,就表示无论是新增操作还是修改操作都会校验该属性,如下:email就新增还是修改操作都会对其进行参数校验,校验是否满足邮箱格式。
@Email(message = “邮箱格式不正确”, groups = {UpdateGroup.class, AddGroup.class})
@ApiModelProperty(value = “邮箱”)
private String email;
如果只加上修改标识不加新增标识的话,这个就只会在修改操作时会触发校验该属性。
@NotNull(message = “用户id不能为空”, groups = UpdateGroup.class)
@ApiModelProperty(value = “id自增列”)
@TableId(value = “id”, type = IdType.AUTO)
private Integer id;
一个实体中,存在嵌套类,举个栗子,如下,可以看到Company对象中含有User对象做为自己的属性,那么在进行Company校验时,user内的属性会被校验吗?如果不会又该怎么做才会进行嵌套类校验呢?
@NotNull(message = "用户对象不能为空", groups = {UpdateGroup.class})
//注解加载bean属性上,表示当前属性不是数据库的字段,但在项目中必须使用,这样在新增等使用bean的时候,mybatis-plus就会忽略这个,不会报错;
@TableField(exist = false)
private UserEntity userEntity;
``很简单,只需要用到@Validated 的嵌套验证即可;
如下图Company类中只能校验UserEntity是否为空,却不能校验UserEntity类中的userName、userPwd等属性是否为空;
如果想要嵌套验证,必须在Company对象的UserEntity字段上面标明这个字段的实体也要进行验证!
由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能;
那么我们能够推断出:@Valid 加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证;
在该需要做嵌套校验的属性上加@Valid 注解即可; 如下:
@Valid
@NotNull(message = “用户对象不能为空”, groups = {UpdateGroup.class})
@TableField(exist = false)
private UserEntity userEntity;
@PostMapping(value = “/updateCompanyInfo”)
@ApiOperation(value = “公司修改”, notes = “公司信息修改”)
public ResultResponse updateCompanyInfo(@Validated(UpdateGroup.class) Company company, BindingResult bindingResult) {
return new ResultResponse<>(iCompanyService.updateById(company));
}
/**
公司信息
*/
@ApiModel(value = “公司信息”, description = “公司信息”)
@Data
public class Company extends BaseEntity {
private static final long serialVersionUID = 1L;
@NotNull(message = “公司id不能为空”, groups = UpdateGroup.class)
@ApiModelProperty(value = “公司id 自增列”)
@TableId(value = “company_id”, type = IdType.AUTO)
private Integer companyId;
@NotBlank(message = “公司名称不能为空”, groups = {UpdateGroup.class, AddGroup.class})
@ApiModelProperty(value = “公司名称”)
private String companyName;
@Valid
@NotNull(message = “用户对象不能为空”, groups = {UpdateGroup.class})
//注解加载bean属性上,表示当前属性不是数据库的字段,但在项目中必须使用,这样在新增等使用bean的时候,mybatis-plus就会忽略这个,不会报错;
@TableField(exist = false)
private UserEntity userEntity;
}
@TableName(value = “user”)
@ApiModel(value = “用户对象”, description = “用户对象”)
@Data
public class UserEntity extends BaseEntity {
private static final long serialVersionUID = 1L;
@NotNull(message = "用户id不能为空", groups = UpdateGroup.class)
@ApiModelProperty(value = "id自增列")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@NotBlank(message = "用户名称不能为空", groups = {UpdateGroup.class, AddGroup.class})
@ApiModelProperty(value = "用户名称")
private String userName;
@Min(value = 1,message = "输入年龄不能小于1", groups = {UpdateGroup.class, AddGroup.class})
@Max(value = 150,message = "输入年龄不能超过150", groups = {UpdateGroup.class, AddGroup.class})
@NotNull(message = "输入年龄不能为空", groups = {UpdateGroup.class, AddGroup.class})
@ApiModelProperty(value = "密码")
private Integer userAge;
@NotBlank(message = "密码不能为空", groups = {UpdateGroup.class, AddGroup.class})
@Length(min = 6, max = 10,message = "密码⻓度⾄少6位但不超过10位!", groups = {UpdateGroup.class, AddGroup.class})
@ApiModelProperty(value = "密码")
private String userPwd;
@Email(message = "邮箱格式不正确", groups = {UpdateGroup.class, AddGroup.class})
@ApiModelProperty(value = "邮箱")
private String email;
}
@RestController
@Api(tags = “测试”)
@RequestMapping("/test")
public class TestController {
@Autowired
private TestService testService;
@Autowired
private ICompanyService iCompanyService;
@PostMapping(value = "/saveUser")
@ApiOperation(value = "用户添加", notes = "用户添加")
public ResultResponse saveUser(@Validated(AddGroup.class) UserEntity userEntity, BindingResult bindingResult) {
/**
* result.hasErrors()如果前端传来的值和在实体类标注的注解不对应就返回false。
* 比如在实体在name字段标注了@NotBlank(message = "用户名称不能为空")而前端没传name就返回了false;
*/
if (bindingResult.hasErrors()) {
Map map = new HashMap<>();
//1、result.getFieldErrors()获取校验的错误结果对象然后遍历
for (FieldError fieldError : bindingResult.getFieldErrors()) {
//FieldError 获取到错误提示
String message = fieldError.getDefaultMessage();
//获取错误的属性的名字
String field = fieldError.getField();
map.put(field, message);
}
return new ResultResponse<>(map);
}
return new ResultResponse<>(testService.save(userEntity));
}
@PostMapping(value = "/update")
@ApiOperation(value = "用户修改", notes = "根据userId对用户信息修改")
public ResultResponse updateUser(@Validated(UpdateGroup.class) UserEntity userEntity, BindingResult bindingResult) {
// if (bindingResult.hasErrors()) {
// Map
// //1、result.getFieldErrors()获取校验的错误结果对象然后遍历
// for (FieldError fieldError : bindingResult.getFieldErrors()) {
// //FieldError 获取到错误提示
// String message = fieldError.getDefaultMessage();
// //获取错误的属性的名字
// String field = fieldError.getField();
// map.put(field, message);
// }
// return new ResultResponse<>(map);
// }
return new ResultResponse<>(testService.updateById(userEntity));
}
@PostMapping(value = "/updateCompanyInfo")
@ApiOperation(value = "公司修改", notes = "公司信息修改")
public ResultResponse updateCompanyInfo(@Validated(UpdateGroup.class) Company company, BindingResult bindingResult) {
// if (bindingResult.hasErrors()) {
// Map
// for (FieldError fieldError : bindingResult.getFieldErrors()) {
// //FieldError 获取到错误提示
// String message = fieldError.getDefaultMessage();
// //获取错误的属性的名字
// String field = fieldError.getField();
// map.put(field, message);
// }
// return new ResultResponse<>(map);
// }
return new ResultResponse<>(iCompanyService.updateById(company));
}
}
/**
校验切片
*/
@Aspect
@Component
public class ValidAspect {
private ObjectMapper objectMapper = new ObjectMapper();
/**
@Order(10)
@Around(“execution(public * com.system.xiaoma.controller..(…))”)
public ResultResponse validAspect1(ProceedingJoinPoint pjp) throws Throwable {
BindingResult bindingResult = getBindingResult(pjp);
if (bindingResult == null || !bindingResult.hasErrors()) {
ResultResponse resultResponse = new ResultResponse();
ResultResponse o = (ResultResponse) pjp.proceed(pjp.getArgs());
resultResponse.setData(o.getData());
return resultResponse;
} else {
Map
List errors = bindingResult.getFieldErrors();
errors.forEach(e -> map.put(e.getField(),e.getDefaultMessage()));
return new ResultResponse(map);
}
}
/**
}
public @interface AddGroup {
}
UpdateGroup
public @interface UpdateGroup {
}