SpringBoot参数校验最全使用
引入maven依赖(可选)
如果我们的项目使用了Spring Boot,hibernate validator框架已经集成在 spring-boot-starter-web中,所以无需再添加其他依赖。如果不是Spring Boot项目,则需要添加如下依赖:
1 <dependency> 2 <groupId>org.hibernate.validator</groupId> 3 <artifactId>hibernate-validator</artifactId> 4 <version>6.0.8.Final</version> 5 </dependency>
常用注解介绍
注解 | 作用类型 | 来源 | 说明 |
@Null | 任何类型 | 属性必须为null | |
@NotNull | 任何类型 | 属性不能为null | |
@NotEmpty | 集合 | hibernate validator扩展注解 | 集合不能为null,且size大于0 |
@NotBlank | 字符串、字符 | hibernate validator扩展注解 | 字符类不能为null,且去掉空格之后长度大于0 |
@AssertTrue | Boolean、boolean | 布尔属性必须是true | |
@Min | 数字类型(原子和包装) | 限定数字的最小值(整型) | |
@Max | 同@Min | 限定数字的最大值(整型) | |
@DecimalMin | 同@Min | 限定数字的最小值(字符串,可以是小数) | |
@DecimalMax | 同@Min | 限定数字的最大值(字符串,可以是小数) | |
@Range | 数字类型(原子和包装) | hibernate validator扩展注解 | 限定数字范围(长整型) |
@Length(min=,max=) | 字符串 | hibernate validator扩展注解 | 限定字符串长度 |
@Size | 集合 | 限定集合大小 | |
@Past | 时间、日期 | 必须是一个过去的时间或日期 | |
@Future | 时期、时间 | 必须是一个未来的时间或日期 | |
字符串 | hibernate validator扩展注解 | 必须是一个邮箱格式 | |
@Pattern | 字符串、字符 | 正则匹配字符串 |
单参数校验
controller类上必须添加@Validated注解,如下所示:
1 @RestController 2 @RequestMapping("/user") 3 @Validated // 需要添加的注解 4 public class UserController { 5 // do something 6 }
方法参数前面加上注解即可, 如下所示:
1 public Result deleteUser(@NotNull(message = "id不能为空") Long id) { 2 // do something 3 }
对象参数校验
对象参数校验使用时,需要先在对象的校验属性上添加注解,然后在Controller方法的对象参数前添加@Validated注解,如下所示:
1 public Result addUser(@RequestBody @Validated UserDto userDto) { 2 // do something 3 } 4 5 public class UserDto { 6 @NotBlank(message="名称不能为空") 7 private String name; 8 9 @NotNull(message="年龄不能为空") 10 private Integer age; 11 12 …… 13 }
嵌套对象校验
如果需要校验的参数对象中还嵌套有一个对象属性,而该嵌套的对象属性也需要校验,那么就需要在该对象属性上增加@Valid注解。
1 public class UserDto { 2 @NotNull 3 private Long id; 4 5 @NotBlank 6 private String name; 7 8 @NotNull 9 private Integer age; 10 11 @Valid 12 private Phone phone; 13 14 …… 15 } 16 17 public class Phone { 18 @NotBlank 19 private String type; 20 21 @NotBlank 22 private String phoneNum; 23 }
分组校验
在对象参数校验场景下,有一种特殊场景,同一个参数对象在不同的场景下有不同的校验规则。比如,在创建对象时不需要传入id字段(id字段是主键,由系统生成,不由用户指定),但是在修改对象时就必须要传入id字段。在这样的场景下就需要对注解进行分组。
1)组件默认有个分组Default.class, 所以我们可以再创建一个分组例如UpdateAction.class,如下所示:
1 public interface UpdateAction { 2 }
2)在参数类中需要校验的属性上,在注解中添加groups属性(表示Default.class默认情况下会校验name,age参数,而在UpdateAction.class情况下会校验id参数),如下所示:
1 public class UserDto { 2 @NotNull(groups = {UpdateAction.class}, message = "id不能为空") 3 private Long id; 4 5 @NotBlank 6 private String name; 7 8 @NotNull 9 private Integer age; 10 11 …… 12 }
3)在controller的方法中,在@Validated注解里指定哪种场景即可(没有指定就代表采用Default.class,采用其他分组则需要显示指定)。如下代码便表示在addUser()接口中按照默认情况进行参数校验,在updateUser()接口中按照默认情况和UpdateAction分组对参数进行共同校验。
1 public Result addUser(@Validated UserDto userDto) { 2 // do something 3 } 4 5 public Result updateUser(@Validated({Default.class, UpdateAction.class}) UserDto userDto) { 6 // do something 7 }
枚举校验
枚举校验hibernate validator并不支持。需要自己扩展实现。
1) 定义EnumChck注解,如下所示:
1 @Documented 2 @Constraint(validatedBy = EnumConstraintValidator.class) 3 @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) 4 @Retention(RUNTIME) 5 @Repeatable(EnumCheck.List.class) 6 public @interface EnumCheck { 7 String message() default "{javax.validation.constraints.EnumCheck.message}"; 8 9 Class<?>[] groups() default { }; 10 11 Class<? extends Payload>[] payload() default { }; 12 13 /** 14 * 枚举类 15 * 16 */ 17 Class<? extends EnumValidator> clazz(); 18 19 /** 20 * 调用的方法名称 21 */ 22 String method() default "getValue"; 23 24 @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) 25 @Retention(RUNTIME) 26 @Documented 27 @interface List { 28 EnumCheck[] value(); 29 } 30 }
2) 定义EnumValidator接口,让需要校验的枚举类实现其接口的getValue()方法,如下所示:
1 public interface EnumValidator { 2 Object getValue(); 3 }
3)自定义实现EnumConstraintValidator,需要实现ConstraintValidator接口,如下所示:
1 public class EnumConstraintValidator implements ConstraintValidator<EnumCheck, Object> { 2 /** 3 * 注解对象 4 */ 5 private EnumCheck annotation; 6 7 /** 8 * 初始化方法 9 * 10 * @param constraintAnnotation 注解对象 11 */ 12 @Override 13 public void initialize(EnumCheck constraintAnnotation) { 14 this.annotation = constraintAnnotation; 15 } 16 17 @Override 18 public boolean isValid(Object value, ConstraintValidatorContext context) { 19 if (Objects.isNull(value)) { 20 return false; 21 } 22 23 Object[] enumConstants = annotation.clazz().getEnumConstants(); 24 try { 25 Method method = annotation.clazz().getMethod(annotation.method()); 26 for (Object enumConstant : enumConstants) { 27 if (value.equals(method.invoke(enumConstant))) { 28 return true; 29 } 30 } 31 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 32 throw new RuntimeException(e); 33 } 34 35 return false; 36 } 37 }
4) 具体使用步骤如下:
具体枚举类实现上面定义的EnumValidator接口:
1 public enum RefNodeType implements EnumValidator { 2 PROJECT("project", "项目"), 3 FIELD("field", "变量"), 4 FUNC("func", "函数"); 5 6 private final String code; 7 private final String label; 8 9 RefNodeType(String code, String label) { 10 this.code = code; 11 this.label = label; 12 } 13 14 public String getCode() { 15 return code; 16 } 17 18 public String getLabel() { 19 return label; 20 } 21 22 @Override // 需要实现getValue方法 23 public Object getValue() { 24 return code; 25 } 26 }
参数校验加上@EnumCheck枚举校验注解,如下所示:
1 public class TestDto { 2 @NotBlank(message = "变量类型不能为空") 3 @EnumCheck(clazz = RefNodeType.class, message = "变量类型不合法") // 加上枚举校验注解 4 private String sourceType; 5 }
正则通用校验
1)定义RegRule接口,将需要用到的正则表达式定义为接口属性,如下所示:
1 public interface RegRule { 2 String MOBILE = "^(((13[0-9])|(14[579])|(15([0-3]|[5-9]))|(16[6])|(17[0135678])|(18[0-9])|(19[89]))\\d{8})$"; 3 }
2)对象属性加上@Pattern注解,如下所示:
1 public class UserDto { 2 @Pattern(regexp = RegRule.MOBILE, message = "手机号格式不正确") 3 private String mobile; 4 }
各类异常捕获处理
参数校验失败后会抛出异常,我们只需要在全局异常处理类中捕获参数校验的失败异常,然后将错误消息添加到返回值中即可。捕获异常的方法如下所示,返回值Result是我们系统自定义的返回值类。
1 @RestControllerAdvice 2 public class GlobalExceptionHandler { 3 /** 4 * 全局异常处理 5 */ 6 @ExceptionHandler(Exception.class) 7 public Result handleException(Exception ex, HttpServletRequest request, HttpServletResponse response) { 8 // do something 9 } 10 }
1)缺少参数抛出的异常是MissingServletRequestParameterException,如下所示:
1 if (e instanceof MissingServletRequestParameterException){ 2 Result result = Result.buildErrorResult(ErrorCodeEnum.PARAM_ILLEGAL); 3 String msg = MessageFormat.format("缺少参数{0}", ((MissingServletRequestParameterException) e).getParameterName()); 4 result.setMessage(msg); 5 return result; 6 }
2)单参数校验失败后抛出的异常是ConstraintViolationException,如下所示:
1 if (e instanceof ConstraintViolationException){ 2 Result result = Result.buildErrorResult(ErrorCodeEnum.PARAM_ILLEGAL); 3 Set<ConstraintViolation<?>> sets = ((ConstraintViolationException) e).getConstraintViolations(); 4 if(CollectionUtils.isNotEmpty(sets)){ 5 StringBuilder sb = new StringBuilder(); 6 sets.forEach(error -> { 7 if (error instanceof FieldError) { 8 sb.append(((FieldError)error).getField()).append(":"); 9 } 10 sb.append(error.getMessage()).append(";"); 11 }); 12 String msg = sb.toString(); 13 msg = StringUtils.substring(msg, 0, msg.length() -1); 14 result.setMessage(msg); 15 } 16 return result; 17 }
3)get请求的对象参数校验失败后抛出的异常是BindException,如下所示:
1 if (e instanceof BindException){ 2 Result result = Result.buildErrorResult(ErrorCodeEnum.PARAM_ILLEGAL); 3 List<ObjectError> errors = ((BindException) e).getBindingResult().getAllErrors(); 4 String msg = getValidExceptionMsg(errors); 5 if (StringUtils.isNotBlank(msg)){ 6 result.setMessage(msg); 7 } 8 9 return result; 10 } 11 12 private String getValidExceptionMsg(List<ObjectError> errors) { 13 if(CollectionUtils.isNotEmpty(errors)){ 14 StringBuilder sb = new StringBuilder(); 15 errors.forEach(error -> { 16 if (error instanceof FieldError) { 17 sb.append(((FieldError)error).getField()).append(":"); 18 } 19 sb.append(error.getDefaultMessage()).append(";"); 20 }); 21 String msg = sb.toString(); 22 msg = StringUtils.substring(msg, 0, msg.length() -1); 23 return msg; 24 } 25 return null; 26 }
4)post请求的对象参数校验失败后抛出的异常是MethodArgumentNotValidException,如下所示:
1 if (e instanceof MethodArgumentNotValidException){ 2 Result result = Result.buildErrorResult(ErrorCodeEnum.PARAM_ILLEGAL); 3 List<ObjectError> errors = ((MethodArgumentNotValidException) e).getBindingResult().getAllErrors(); 4 String msg = getValidExceptionMsg(errors); 5 if (StringUtils.isNotBlank(msg)){ 6 result.setMessage(msg); 7 } 8 9 return result; 10 }