Springboot 参数校验
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
,则需要手动引入依赖:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
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;
}
/**
* 在方法参数上使用@Valid声明校验注解(使用@Valid和@Validated都可以)
*/
@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 {
/**
* requestBody 接收参数,校验失败统一处理
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException exception){
BindingResult bindingResult = exception.getBindingResult();
List<FieldError> 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);
}
/**
* requestParam/PathVariable 接收参数,校验失败统一处理
*/
@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
*/
@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 {
/**
* 在方法参数上使用@Valid声明校验注解
*/
@PostMapping("/brand")
public Result saveBrand(@RequestBody @Valid BrandDto brand){
return brandService.saveBrand(brand);
}
}
集合校验
如果请求体直接传递了json
数组给后台,并希望对数组中的每一项都进行参数校验。此时要使用自定义 list
集合来接收参数:
@Data
public class ValidList<E> implements List<E> {
@Delegate
@Valid
public List<E> list = new ArrayList<>();
}
@RestController
@RequestMapping("/v1/web/brand")
public class BrandController {
/**
* 集合校验
*/
@PostMapping("/brands")
public Result saveBrands(@RequestBody @Valid ValidList<BrandDto> 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
接口编写校验器,制定校验规则:
/**
* 自定义注解处理类必须实现 ConstraintValidator接口, 其中BrandName是自定义的注解, 而String是注解标注参数的类型
*/
public class BrandNameValidator implements ConstraintValidator<BrandName, String> {
private String value;
private static List<String> 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();
}
/**
* 校验逻辑,true:通过自定义注解的校验;false:没有通过自定义注解的校验,并返回自定义注解中 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 表示校验此注解的校验器类,可以有多个
*/
@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、运行结果如下: