Java API规范(JSR303)定义了Bean校验的标准 validation-api,但没有提供实现。hibernate validation 是对这个规范的实现,并增加了校验注解如 @Email、@Length 等。
Spring Validation 是对 hibernate validation 的二次封装,用于支持 spring mvc 参数自动校验。
引入依赖
如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。如果spring-boot版本大于2.3.x,则需要手动引入依赖:
org.hibernate hibernate-validator 6.0.1.Final
requestBody 参数校验
POST、PUT请求一般会使用requestBody传递参数,这种情况下,只要给 DTO 对象加上 @Validated 注解就能实现自动参数校验。
比如,有一个保存Brand的接口,要求 name 长度是不能超过30字符。
@Data
public class BrandDto {
private Long id;
@NotBlank(message = "品牌名不能为空!")
@Length(max = 30, message = "品牌名太长了!")
private String name;
private String chineseSpell;
private String englishName;
}
@PostMapping("/brand")
public Result saveBrand(@RequestBody @Valid BrandDto brand){
return brandService.saveBrand(brand);
}
校验失败,会抛出 MethodArgumentNotValidException 异常,Spring默认会将其转为400(Bad Request)请求。
requestParam/PathVariable 参数校验
如果是 GET 请求一般会使用 requestParam/PathVariable 传参,此时要在类上标注 @Validated 注解,并在入参上声明约束注解。
@RestController
@RequestMapping("/v1/web/brand")
@Validated
public class BrandController {
@Autowired
private BrandService brandService;
@GetMapping("/brand")
public Result getBrand(@RequestParam("id")
@NotNull(message = "id不能为空!")
@Max(value = 10000, message = "id超过最大范围了!") Long id){
return brandService.getBrand(id);
}
}
校验失败,会抛出 ConstraintViolationException 异常:
统一异常处理
如果校验失败,会抛出 MethodArgumentNotValidException 或者 ConstraintViolationException 异常。在实际项目开发中,通常会用统一异常处理根据业务情况来返回一个更友好的提示:
// 等价于 @ControllerAdvice + @ResponseBody
@RestControllerAdvice
public class GlobalExectionHandler {
@ExceptionHandler({MethodArgumentNotValidException.class})
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException exception){
BindingResult bindingResult = exception.getBindingResult();
List fieldErrorList = bindingResult.getFieldErrors();
int size = fieldErrorList.size();
StringBuilder sb = new StringBuilder("参数校验失败:");
for (int i = 0; i < size; i++) {
FieldError fieldError = fieldErrorList.get(i);
sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage());
if(i != (size-1)){
sb.append(", ");
}
}
String msg = sb.toString();
return Result.parameterError().setMessage(msg);
}
@ExceptionHandler({ConstraintViolationException.class})
public Result handleConstraintViolationException(ConstraintViolationException exception) {
return Result.parameterError().setMessage("参数校验失败:" + exception.getMessage());
}
}
快速失败
Spring Validation 默认会校验完所有字段,然后才抛出异常。可以开启 Fali Fast 模式,一旦校验失败就立即返回。
@Configuration
public class ValidateConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(Hibernatevalidator.class)
.configure()
// 快速失败模式
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
嵌套校验
实际场景中,可能存在一个实体类中还嵌套着另外的实体类,即一个对象的某个属性是对另一个对象的引用。这种情况下可以使用嵌套校验,此时,该属性字段上必须加上 @Valid注解,下面是一个具体的例子:
@Data
public class BrandDto {
private Long id;
@NotBlank(message = "品牌名不能为空!")
@Length(max = 30, message = "品牌名太长了!")
private String name;
private String chineseSpell;
private String englishName;
@Valid
private EnterpriseBrandDto enterpriseBrandDto;
}
@Data
public class EnterpriseBrandDto {
private Long id;
@NotNull(message = "企业账号id不能为空!")
private Long enterpriseId;
@NotNull(message = "品牌id不能为空!")
private Long brandId;
private Date createDate;
private Date updateDate;
}
@RestController
@RequestMapping("/v1/web/brand")
public class BrandController {
@PostMapping("/brand")
public Result saveBrand(@RequestBody @Valid BrandDto brand){
return brandService.saveBrand(brand);
}
}
集合校验
如果请求体直接传递了json 数组给后台,并希望对数组中的每一项都进行参数校验。此时要使用自定义 list 集合来接收参数:
@Data public class ValidListimplements List { @Delegate @Valid public List list = new ArrayList<>(); } @RestController @RequestMapping("/v1/web/brand") public class BrandController { @PostMapping("/brands") public Result saveBrands(@RequestBody @Valid ValidList brands){ return brandService.saveBrands(brands); } }
自定义校验
spring validation 支持扩展,可以根据实际业务场景,定制化校验规则。
自定义校验主要分两步进行:
1、自定义校验注解:
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@documented
public @interface BrandName {
String message() default "品牌名不合法!";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
2、实现 ConstraintValidator 接口编写校验器,制定校验规则:
public class BrandNamevalidator implements ConstraintValidator{ private String value; private static List brandList = new ArrayList<>(); static{ brandList.add("H&M"); brandList.add("范思哲"); brandList.add("纪梵希"); brandList.add("施华洛世奇"); brandList.add("杜嘉班纳"); } @Override public void initialize(BrandName brandName) { this.value = brandName.message(); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if(StringUtils.isEmpty(value)){ return false; } if(brandList.contains(value)){ // 禁止默认消息返回 context.disableDefaultConstraintViolation(); //自定义返回消息 context.buildConstraintViolationWithTemplate("拒绝与辱华品牌合作,谢谢理解!").addConstraintViolation(); return false; } return true; } }
3、在自定义注解上面指定使用的校验器类(通过@Constraint 指定):
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@documented
@Constraint(validatedBy = {BrandNamevalidator.class})
public @interface BrandName{
String message() default "品牌名不合法!";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
4、使用自定义校验注解:
@Data
public class BrandDto {
private Long id;
@BrandName
private String name;
}
5、运行结果如下:



