1.为什么需要参数校验
在接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,靠代码对接口参数一个个校验的话就太繁琐了,
Validator框架就是为了解决开发人员在开发的时候少写代码,提升开发效率;Validator专门用来进行接口参数校验,例如常见的非空、集合个数、数值范围校验等等…
2.SpringBoot中集成参数校验
2.1 引入依赖
注:从springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入validation和web,而springboot-2.3之前的版本只需要引入 web 依赖就可以了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2.2 定义校验器配置
使用时可以不配置这个类,默认的校验模式就是全部参数都校验一遍,然后返回整体结果,此处配置设置校验模式为快速失败,单个校验不通过就结束校验返回结果
@Configuration(proxyBeanMethods = false)
public class ValidatorConfiguration {
@Bean
@ConditionalOnMissingBean(Validator.class)
public LocalValidatorFactoryBean validator(AutowireCapableBeanFactory springFactory) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
// 快速失败
factoryBean.getValidationPropertyMap().put(BaseHibernateValidatorConfiguration.FAIL_FAST, Boolean.TRUE.toString());
return factoryBean;
}
}
2.3 定义需要参数校验的实体类
@Data
public class InfoRequest {
@ApiModelProperty("主键id")
@NotNull(message = "主键,不能为空")
private Integer id;
@ApiModelProperty("性别")
@NotBlank(message = "性别,不能为空")
@Pattern(regexp = "^(男|女){1}$",message = "性别只允许输入 男、女")
private String sex;
@ApiModelProperty("姓名")
@NotBlank(message = "姓名,不能为空")
@Length(min = 2,max = 5)
private String name;
@ApiModelProperty("车辆列表")
@NotEmpty(message = "车辆列表,不能为空")
private List<String> carList;
@ApiModelProperty("记录列表")
@NotEmpty(message = "记录列表,不能为空")
@Size(min = 2, max = 3, message = "记录列表,个数必须在2和3之间")
private Set<String> ids;
@ApiModelProperty("邮件地址")
@Email(message = "邮件地址,不正确")
private String email;
@ApiModelProperty("邮件地址")
@IdCard(message = "身份证号不正确")
private String idCardNumber;
@ApiModelProperty("年龄")
@Max(value = 100, message = "最大年龄100岁")
@Min(value = 1, message = "最小年龄1岁")
private Integer age;
@ApiModelProperty("车辆数")
@Range(min = 0, max = 2, message = "车辆数超出范围")
private String carNumber;
@ApiModelProperty("内嵌参数")
@NotNull(message = "内嵌参数不能为空")
@Valid
private InnerParam innerParam;
@Data
public static class InnerParam{
@NotNull(message = "内嵌参数id不能为空")
private Integer id;
@NotEmpty(message = "内嵌参数ids集合,不能为空")
private Set<String> ids;
}
}
2.4 定义需要参数校验的实体类
2.4.1 校验对象参数
@RestController
@RequestMapping("validation")
@Api(tags = "参数验证学习")
public class VerifyController {
@Autowired
private Validator validator;
@ApiOperation(value = "对象参数校验接口")
@PostMapping("checkObject")
// 此处使用@Valid 或者 @Valided 都可以
public JsonResult checkObject(@RequestBody @Valid InfoRequest infoRequest) {
return JsonResult.success();
}
}
2.4.2 校验单个参数
@RestController
@RequestMapping("validation")
@Api(tags = "参数验证学习")
@Validated // 步骤一 类标记校验注解
public class VerifyController {
@ApiOperation(value = "单个参数校验接口")
@PostMapping("checkSingleParams") // 步骤二 参数前标记约束校验注解
public JsonResult checkSingleParams(@NotBlank(message = "主键id不能为空") @RequestParam("id1") String id) {
return JsonResult.success();
}
}
2.4.3 手动执行校验逻辑
@RestController
@RequestMapping("validation")
@Api(tags = "参数验证学习")
public class VerifyController {
@Autowired
private Validator validator;
@ApiOperation(value = "手动执行校验接口")
@PostMapping("handCheck")
public JsonResult<String> handCheck(@RequestBody InfoRequest infoRequest) {
Set<ConstraintViolation<InfoRequest>> set = validator.validate(infoRequest);
return JsonResult.success(set.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(":")));
}
}
2.5 自定义参数校验
虽然Spring Validation 提供的注解基本上够用,但是面对复杂的定义,我们还是需要自己定义相关注解来实现自动校验。
2.5.1 创建自定义校验注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(IdCard.List.class)
@Documented
@Constraint(validatedBy = {IdCardValidator.class}) // 此处关联自定义校验的实现类
public @interface IdCard {
String message() default "身份证格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
IdCard[] value();
}
}
2.5.2 实现校验逻辑
public class IdCardValidator implements ConstraintValidator<IdCard, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (value == null) {
return false;
}
String regular = "";
if (value.length() == 15) {
regular = "^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}$";
} else if (value.length() == 18) {
regular = "^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";
} else {
return false;
}
return Pattern.compile(regular).matcher(value).find();
}
}
2.5.3 在字段上使用自定义校验的注解
@ApiModelProperty("邮件地址")
@IdCard(message = "身份证号不正确")
private String idCardNumber;
2.5.3 使用效果
2.6 优雅捕获校验异常
参数校验失败,会抛校验失败异常,如果不做异常捕获,返回结果就不那么优雅了,如下展示
@ExceptionHandler(value = {MethodArgumentNotValidException.class, ValidationException.class, BindException.class})
public JsonResult methodArgumentNotValidException(Throwable e) {
StringBuilder sb = new StringBuilder();
BindingResult bindingResult = null;
if (e instanceof MethodArgumentNotValidException) {
bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
bindingResult.getFieldErrors().forEach(fieldError -> sb.append(fieldError.getDefaultMessage()));
} else if (e instanceof ValidationException) {
if (e instanceof ConstraintViolationException) {
Set<ConstraintViolation<?>> violations = ((ConstraintViolationException) e).getConstraintViolations();
String message = violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
sb.append(message);
} else {
sb.append(e.getMessage());
}
} else if (e instanceof BindException) {
bindingResult = ((BindException) e).getBindingResult();
bindingResult.getFieldErrors().forEach((fieldError) -> sb.append(fieldError.getDefaultMessage()));
}
return JsonResult.fail(1001, sb.toString());
}