如何在后端中摆脱if进行参数校验?JSR303规范提供了一系列的参数校验注解帮我们解决常见的参数校验
首先需要在我们的pom.xml文件中引入相关依赖
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.1.Final</version>
</dependency>
1.JSR303提供方的校验注解
在javax.validation.constraints包中,为我们提供了以下注解,至于每个注解的用法,可以到源码中查看注释说明
2.参数校验注解的使用方法
2.1在实体类属性上加注解
package com.bigbear.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
/**
* 品牌
*
* @author
* @email
* @date 2020-08-14 04:01:06
*/
@Data
@TableName("pms_brand")
public class PmsBrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotNull(message = "品牌名称不能为空")
private String name;
/**
* 品牌logo地址
*/
@URL(message = "logo地址不合法")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@Pattern(regexp = "/^[a-zA-Z]$/", message = "检索首字母非法")
private String firstLetter;
/**
* 排序
*/
@Min(value = 0, message = "排序必须为非负整数")
private Integer sort;
}
以@NotBlank注解为例,此注解有几个属性,其中message属性是参数校验不通过时的提示信息,如果我们不指定message的值,默认从ValidationMessage.properties文件中获取
/*
* Bean Validation API
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package javax.validation.constraints;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.NotNull.List;
/**
* The annotated element must not be {@code null}.
* Accepts any type.
*
* @author Emmanuel Bernard
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
/**
* Defines several {@link NotNull} annotations on the same element.
*
* @see javax.validation.constraints.NotNull
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
NotNull[] value();
}
}
2.2在controller的方法中参数加上@Valid注解
package com.bigbear.product.controller;
import java.util.Arrays;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.bigbear.product.entity.PmsBrandEntity;
import com.bigbear.product.service.PmsBrandService;
import com.bigbear.common.utils.PageUtils;
import com.bigbear.common.utils.R;
import javax.validation.Valid;
/**
* 品牌
*
* @author
* @email
* @date 2020-08-14 04:01:06
*/
@RestController
@RequestMapping("product/pmsbrand")
public class PmsBrandController {
@Autowired
private PmsBrandService pmsBrandService;
/**
* 保存
*/
@RequestMapping("/save")
public R save(@RequestBody @Valid PmsBrandEntity pmsBrand){
pmsBrandService.save(pmsBrand);
return R.ok();
}
}
注意:这里的参数校验只是在请求有提交相应的属性参数时对属性进行校验,如果没有提交相应的属性参数,就不会对属性参数 做校验,比如我们使用@URL注解对图片url是否是合法的url做校验,如果请求的参数带有logo参数,后台会对这个参数 做校验,但是如果没有带上logo参数,校验就不会生效。这样我们就需要组合校验注解
package com.bigbear.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
/**
* 品牌
*
* @author
* @email
* @date 2020-08-14 04:01:06
*/
@Data
@TableName("pms_brand")
public class PmsBrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotNull(message = "品牌名称不能为空")
private String name;
/**
* 品牌logo地址
*/
@URL(message = "logo地址不合法")
@NotNull
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotNull
@Pattern(regexp = "/^[a-zA-Z]$/", message = "检索首字母非法")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0, message = "排序必须为非负整数")
private Integer sort;
}
3.统一处理参数校验的异常处理
参数出现问题时,我们需要返回系统定义的统一的数据返回格式,如果在每个需要校验参数的controller方法中都去对异常参数进行处理返回统一的返回格式,显得代码很冗余,这样我们需要进行统一处理
package com.bigbear.common.exception;
import lombok.Getter;
@Getter
public enum BizCodeEnum {
VALID_EXCEPTION(10001, "参数校验异常"),
UNKNOWN_EXCEPTION(10000, "未知异常");
private Integer code;
private String message;
BizCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
package com.bigbear.product.exception;
import com.bigbear.common.exception.BizCodeEnum;
import com.bigbear.common.utils.R;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
/**
* 统一异常处理
*/
@RestControllerAdvice(basePackages = "com.bigbear.product.controller")
@ResponseBody
@Slf4j
public class ExceptionHandle {
@ExceptionHandler(value = {MethodArgumentNotValidException.class})
public R handleValidException(MethodArgumentNotValidException e) {
log.error("参数校验异常{},异常类型{}", e.getMessage(), e.getClass());
BindingResult bindResult = e.getBindingResult();
Map<String, String> errorMap = Maps.newHashMap();
bindResult.getFieldErrors().forEach(fieldError -> {
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
});
return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(), BizCodeEnum.UNKNOWN_EXCEPTION.getMessage()).put("data", errorMap);
}
@ExceptionHandler(value = {Throwable.class})
public R handleException() {
return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(), BizCodeEnum.UNKNOWN_EXCEPTION.getMessage());
}
}
4.分组校验
在我们的业务中,经常会对某个对象进行新增和修改操作,这时候,我们前端发起的请求携带的参数在后端往往可以使用同一个类对象接收,但是我们新增和修改操作往往对参数的校验规则不一样,这时候我们可能就会使用的分组校验功能
比如上面我们例举的品牌类,当执行新增操作时,id主键是后台或数据库生成的,这时候我们不需要携带id参数,但是在执行修改操作时,却必须要通过主键在数据库找到对应的数据进行修改,而品牌名称无论是在执行新增或修改操作都不能缺少
package com.bigbear.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import com.bigbear.common.valid.BrandAddGroup;
import com.bigbear.common.valid.BrandUpdateGroup;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Pattern;
/**
* 品牌
*
* @author
* @email
* @date 2020-08-14 04:01:06
*/
@Data
@TableName("pms_brand")
public class PmsBrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
@NotNull(message = "品牌ID不能为空", groups = {BrandUpdateGroup.class}) // 执行修改操作时,品牌id不能为空
@Null(message = "品牌ID不为空", groups = {BrandAddGroup.class}) // 执行新增操作时,品牌id必须为空
private Long brandId;
/**
* 品牌名
*/
@NotNull(message = "品牌名称不能为空")
private String name;
/**
* 品牌logo地址
*/
@URL(message = "logo地址不合法")
@NotNull
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotNull
@Pattern(regexp = "/^[a-zA-Z]$/", message = "检索首字母非法")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0, message = "排序必须为非负整数")
private Integer sort;
}
4.1写分组的空接口
package com.bigbear.common.valid;
/**
* 品牌新增参数分组校验
*/
public interface BrandAddGroup {
}
package com.bigbear.common.valid;
/**
* 品牌修改分组校验
*/
public interface BrandUpdateGroup {
}
4.2在controller的方法参数中指定分组
package com.bigbear.product.controller;
import java.util.Arrays;
import java.util.Map;
import com.bigbear.common.valid.BrandAddGroup;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.bigbear.product.entity.PmsBrandEntity;
import com.bigbear.product.service.PmsBrandService;
import com.bigbear.common.utils.PageUtils;
import com.bigbear.common.utils.R;
import javax.validation.Valid;
/**
* 品牌
*
* @author
* @email
* @date 2020-08-14 04:01:06
*/
@RestController
@RequestMapping("product/pmsbrand")
public class PmsBrandController {
@Autowired
private PmsBrandService pmsBrandService;
/**
* 保存
*/
@RequestMapping("/save")
public R save(@RequestBody @Validated(value = {BrandAddGroup.class}) PmsBrandEntity pmsBrand){
pmsBrandService.save(pmsBrand);
return R.ok();
}
}
注意:当我们使用分组校验时(使用了@Validated注解而不是@Valid),当实体类中的注解不指定分组时,校验将不会生效, 比如我们的实体类中的logo属性的@URL校验注解,我们未指定分组,那么则当前端传给后台带了logo参数,后台也不会 校验,所以这时我们必须要指定分组,但是未指定分组的参数校验,在Controller方法的参数中使用@Valid时,是会生效 的
所以我们的实体类要如下编写
package com.bigbear.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import com.bigbear.common.valid.BrandAddGroup;
import com.bigbear.common.valid.BrandUpdateGroup;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Pattern;
/**
* 品牌
*
* @author
* @email
* @date 2020-08-14 04:01:06
*/
@Data
@TableName("pms_brand")
public class PmsBrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
@NotNull(message = "品牌ID不能为空", groups = {BrandUpdateGroup.class}) // 执行修改操作时,品牌id不能为空
@Null(message = "品牌ID不为空", groups = {BrandAddGroup.class}) // 执行新增操作时,品牌id必须为空
private Long brandId;
/**
* 品牌名
*/
@NotNull(message = "品牌名称不能为空", groups = {BrandAddGroup.class, BrandUpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotNull(message = "品牌图片地址不能为空", groups = {BrandAddGroup.class})
@URL(message = "logo地址不合法", groups = {BrandAddGroup.class, BrandUpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotNull(groups = {BrandAddGroup.class})
@Pattern(regexp = "/^[a-zA-Z]$/", message = "检索首字母非法")
private String firstLetter;
/**
* 排序
*/
@NotNull(groups = {BrandAddGroup.class})
@Min(value = 0, message = "排序必须为非负整数")
private Integer sort;
}
5.自定义注解校验
有时候,提供给我们的校验注解都不合适对我们的参数进行校验时,我们可以采用自定义注解的方式实现参数校验,比如我们的showStatus属性,值是特定的,这时候我们需要用自定义注解实现校验(虽然正则也能校验,为了学习自定义注解校验,就用这个做法来做)
5.1编写自定义校验注解
package com.bigbear.common.valid;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Documented
public @interface ListValue {
String message() default "{com.bigbear.common.valid.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
/**
* @return value the element must be higher or equal to
*/
int[] value();
}
5.2编写ValidationMessages.propertie文件
文件放在resources目录下
ListValue.message=数据不在指定值范围内
5.3编写自定义校验器
package com.bigbear.common.valid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
private static Set set = new HashSet();
@Override
public void initialize(ListValue listValue) {
int[] value = listValue.value();
for (Object o : value) {
set.add(o);
}
}
/**
*
* @param value 需要校验的数据
* @param constraintValidatorContext
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
if (set.contains(value)) {
return true;
}
return false;
}
}
5.4将自定义校验器和自定义注解关联
@Constraint(validatedBy = { ListValueConstraintValidator.class })