- 一、相关概念
- 1.为什么要进行统一参数校验
- 2. 统一参数校验框架
- 3. 统一参数校验基本流程
- 4. @Validated 和 @Valid 的区别
- 二、不同类型参数校验举例
- 1. 简单参数校验
- 2. 嵌套校验
- 3. 分组校验
- 4. 自定义校验
- 5. 多属性交叉校验
通常为保证接口的安全性,后端接口需要对入参进行合法性校验,如所填参数是否为空、参数长度是否在规定范围内、手机号或邮箱是否为正确格式等。因此,不可避免在业务代码中进行手动校验,但这种方式使得代码臃肿,可读性差。在Spring中提供了统一参数校验框架,将校验逻辑与业务逻辑进行隔离,进而优雅处理这个问题,简化开发人员负担
在Spring中,其自身不仅拥有独立的参数校验框架,还支持符合JSR303标准的框架。JSR303是java为Bean数据合法性校验提供的标准框架,通过在Bean属性上标注注解,来设置对应的校验规则,下表展示JSR303部分注解
Spring本身并没有提供JSR303标准的实现,在使用JSR303参数校验时,需要引入具体的第三方实现。 Hibernate Validator是一个符合JSR303标准的官方参考实现,除支持以上所有标准注解外,它还支持以下的扩展注解
当然,若以上校验注解均无法满足实际校验需求,可以通过实现ConstraintValidator接口,来自定义注解和校验规则。
- 首先需要在Spring容器中创建一个LocalValidatorFactoryBean(SpringBoot会自动装配好一个LocalValidateFactoryBean)
- 然后在已标注校验注解的入参bean对象前加上一个@Valid,表明需要被校验,当Spring MVC框架将请求参数绑定到该bean对象后,交由spring校验框架进行处理
- 根据注解声明的校验规则进行校验,若不合法抛出javax.validation.ConstraintViolationException异常。另外也可将校验的结果保存到随后的入参中,入参必须是BindingResult或Errors类型,并通过对应方法获取到校验失败的字段及信息(需要被校验的入参bean对象和接收校验结果的对象是成对出现,它们之间不允许声明其它入参)
在Spring参数校验框架中增加了一个@Validated注解(它是JSR303的一个变种,对JSR303进行一定程度的封装和扩展),@Validated和@Valid在进行基本验证功能上没有太多区别,但它们主要区别在于注解范围、分组校验、嵌套校验上有所不同
-
@Validatd注解范围可以用在类、方法和方法参数上,但是不能用在成员属性(字段)上,而@Valid可以用在类、方法、方法参数和成员属性(字段)上
-
@Validatd提供了分组校验功能,而@Valid未提供分组校验功能
-
@Validated和@Valid单独加在方法参数前,都不会自动对参数进行嵌套检验,因为@Valid可应用在成员属性上,通过添加该注解,配合@Validated或者@Valid来进行嵌套校验
-
定义一个简单的入参类,作为用户注册时入参所绑定的bean对象
@Data public class AdminVo { private Long id; @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "密码不能为空") private String password; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") //入参格式化 private Date createTime; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; }
-
定义controller类,创建一个新增用户接口
@RestController public class AdminController { @Autowired AdminService adminService; @PostMapping("/add") public Long add(@Validated @RequestBody AdminVo adminVo) { Admin admin = new Admin(); BeanUtils.copyProperties(adminVo, admin); Date date = new Date(); admin.setCreateTime(date); admin.setUpdateTime(date); adminService.save(admin); return admin.getId(); } }
-
定义一个全局异常处理类,用来接收参数校验异常的结果
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value = Exception.class) public R
-
启动服务,若请求内容均为空如{},返回如下结果
{ "code": -1, "data": [ "username 用户名不能为空", "password 密码不能为空" ], "msg": " *** 作失败" }
-
当入参对象包含复杂属性时需要进行嵌套校验。如用户包含多个角色,不仅需要对用户基本信息校验,还要对所填角色进行校验,修改入参对象并添加角色列表,@Valid作用于成员属性roleVoList上
@Data public class AdminVo { private Long id; @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "密码不能为空") private String password; @Valid private List
roleVoList; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") //入参格式化 private Date createTime; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; } @Data public class RoleVo { private Long id; @NotBlank(message = "角色名不能为空") private String RoleName; } -
启动服务,请求内容中添加角色信息roleVoList为空
{ "username": "wsp", "password": "123456", "roleVoList":[{}] }
-
返回如下结果
{ "code": -1, "data": [ "roleVoList[0].RoleName 角色名不能为空" ], "msg": " *** 作失败" }
-
当多个接口的入参对象为同一个,但是校验规则不同,此时需要使用分组校验。对用户进行修改时,保证传入用户id不能为空,并指定分组类型为Update.class(不设置分组类型时,会给定一个默认分组类型为Default.class)
@Data public class AdminVo { @NotNull(groups = {Update.class}) private Long id; @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "密码不能为空") private String password; @Valid private List
roleVoList; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") //入参格式化 private Date createTime; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; } -
创建一个修改用户接口,定义校验分组类型为Update.class,只会找入参对象中相同分组类型的属性进行校验,若需要对其它未设置分组的属性进行校验,因为默认的分组类型为Default.class,可以定义校验的分组类型为@Validated({Update.class,Default.class}
@PostMapping("/update") public Long update(@Validated({Update.class}) @RequestBody AdminVo adminVo) { Admin admin = new Admin(); BeanUtils.copyProperties(adminVo, admin); admin.setUpdateTime(new Date()); adminService.updateById(admin); return admin.getId(); }
-
启动服务,请求内容中不填用户id
{ "username":"wsp", "password":"123456", "roleVoList":[{"roleName":"管理员"}] }
-
返回结果如下
{ "code": -1, "data": [ "id 不能为null" ], "msg": " *** 作失败" }
-
当基本注解无法满足实际需求时,需要自定义注解校验。如根据用户名批量删除用户,所提供@NotEmpty只能保证入参集合不能为空,无法保证集合中元素为空即用户名为空字符串的情况,首先定义一个自定义注解
@documented @Constraint(validatedBy = {NotEmptyValidatorForCollection.class}) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Repeatable(List.class) public @interface NotEmptyForCharSequence { String message() default "{javax.validation.constraints.NotEmpty.message}"; Class>[] groups() default { }; Class extends Payload>[] payload() default { }; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @documented public @interface List { NotEmptyForCharSequence[] value(); } }
-
具体的校验规则由NotEmptyValidatorForCollection.class实现
public class NotEmptyValidatorForCollection implements ConstraintValidator
{ public NotEmptyValidatorForCollection() { } public boolean isValid(Collection collection, ConstraintValidatorContext constraintValidatorContext) { //集合不能为空,且集合中不能为空字符串 if (collection == null || checkEmptyElement(collection)) { return false; } else { return collection.size() > 0; } } private boolean checkEmptyElement(Collection collection) { for (String s : collection) { if(StringUtils.isBlank(s)){ return true; } } return false; } } -
创建一个根据用户名批量删除用户的接口,并添加自定义注解@NotEmptyForCharSequence (为保证注解生效,此接口的Contoller类上需要添加@Validated注解)
@RestController @Validated public class AdminController { @Autowired AdminService adminService; @DeleteMapping("/batchDelete") public boolean batchDeleteByUserName(@RequestBody @NotEmptyForCharSequence List
userNameList) throws Exception { return adminService.remove(Wrappers.lambdaQuery().in(Admin::getUsername,userNameList)); } } -
在接口方法中指定具体校验注解,校验异常默认为ConstraintViolationException.class,需要在全局异常类中捕获该异常,在全局异常类GlobalExceptionHandler.class的handleBadRequest(Exception e)方法中添加如下方法
if (e instanceof ConstraintViolationException){ //校验结果 List
errorList = new ArrayList<>(); Set > constraintViolations = ((ConstraintViolationException) e).getConstraintViolations(); if (CollUtil.isNotEmpty(constraintViolations)){ constraintViolations.forEach(constraintViolation -> { Path propertyPath = constraintViolation.getPropertyPath(); String message = constraintViolation.getMessage(); errorList.add(propertyPath.toString()+" "+message); }); } return R.restResult(errorList, ApiErrorCode.FAILED); } -
启动服务,请求内容中部分用户为空字符串
[ "1"," " ]
-
返回结果如下
{ "code": -1, "data": [ "batchDeleteByUserName.userNameList 不能为空" ], "msg": " *** 作失败" }
-
以上都是单个属性校验,当入参对象中多个不同属性需要进行交叉校验,可以通过脚本执行器@scriptAssert执行脚本或者自定义注解实现。如要查询指定时间范围内所创建用户列表时,要保证入参的开始时间小于等于结束时间,定义一个条件查询对象
@Data @scriptAssert(lang = "javascript", script = "com.wsp.validate.model.AdminCondition.checkTime(_this.startTime,_this.endTime)", message = "开始日期不能大于结束日期") public class AdminCondition { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date startTime; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date endTime; //脚本校验(为静态方法) public static boolean checkTime(String startTime,String endTime){ if(startTime.compareTo(endTime) > 0){ return false; } return true; } }
-
创建一个根据用户注册日期查询用户列表的接口
@PostMapping("/search") public List searchByRangeOfDate(@Validated @RequestBody AdminCondition adminCondition){ return adminService .list(WrapperslambdaQuery().between(Admin::getCreateTime, adminCondition.getStartTime(),adminCondition.getEndTime())); }
-
启动服务,请求内容中开始时间大于结束时间
{ "startTime": "2021-10-10 22:00:00", "endTime":"2021-10-10 21:37:00" }
-
返回结果如下
{ "code": -1, "data": [ "开始日期不能大于结束日期" ], "msg": " *** 作失败" }
-
另外,还可以使用自定义注解方式来实现,首先自定义一个注解
@documented @Constraint(validatedBy = {CheckTimeIntervalValidator.class}) @Target({TYPE, METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Repeatable(CheckTimeInterval.List.class) public @interface CheckTimeInterval { String startTime() default "startTime"; //所需校验的属性为startTime String endTime() default "endTime"; //所需校验的属性为endTime String message() default "开始时间不能大于结束时间"; Class>[] groups() default { }; Class extends Payload>[] payload() default { }; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @documented public @interface List { CheckTimeInterval[] value(); } }
-
具体校验规则由CheckTimeIntervalValidator.class实现
public class CheckTimeIntervalValidator implements ConstraintValidator
{ private String startTime; private String endTime; @Override public void initialize(CheckTimeInterval constraintAnnotation) { this.startTime = constraintAnnotation.startTime(); this.endTime = constraintAnnotation.endTime(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { if(null == value){ return false; } //取对象属性值 BeanWrapperImpl beanWrapper = new BeanWrapperImpl(value); Object start = beanWrapper.getPropertyValue(startTime); Object end = beanWrapper.getPropertyValue(endTime); if(((Date)start).compareTo((Date) end) > 0){ return false; } return true; } } -
创建一个根据用户注册日期查询用户列表接口,并添加@CheckTimeInterval校验注解(为保证该注解生效,Controller类上需要添加@Validated)
@PostMapping("/search") public List searchByRangeOfDate(@CheckTimeInterval @RequestBody AdminCondition adminCondition){ return adminService.list(Wrappers.lambdaQuery().between(Admin::getCreateTime, adminCondition.getStartTime(),adminCondition.getEndTime())); }
-
启动服务,若请求内容中开始时间大于结束时间
{ "startTime": "2021-10-10 23:00:00", "endTime":"2021-10-10 21:37:00" }
-
返回结果如下
{ "code": -1, "data": [ "searchByRangeOfDate.adminCondition 开始时间不能大于结束时间" ], "msg": " *** 作失败" }
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)