1 需求 1.1 什么是 JSR-303 在 JavaWeb 项目开发中,常常需要进行接口参数校验,这个需求在 JSR-303规范中被提到,JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation,官方参考实现是 Hibernate Validator。
1.2 hibernate-validator 简介 hibernate-validator 是 JSR-303 规范的一个实现,也是 Java 开发中使用最广泛的参数校验框架,而它也被集成到了 Spring 家族中 spring-boot-starter-validation ,你可以轻易的使用它来完成接口参数校验。使用文档参见 Hibernate Validator 。
2 使用前置 2.1 引入依赖 Maven 项目中引入以下任一坐标即可:
1 2 3 4 5 <dependency > <groupId > org.hibernate.validator</groupId > <artifactId > hibernate-validator</artifactId > <version > 6.2.0.Final</version > </dependency >
或
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > <version > 2.5.5</version > </dependency >
2.2 常用注解
注解
可验证的数据类型
说明
@AssertFalse
Boolean,boolean
验证注解的元素值是false
@AssertTrue
Boolean,boolean
验证注解的元素值是true
@NotNull
任意类型
验证注解的元素值不是null
@Null
任意类型
验证注解的元素值是null
@NotEmpty
CharSequence子类型、Collection、Map、数组
验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotBlank
CharSequence子类型
验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@Min(value=值)
BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型
验证注解的元素值大于等于@Min指定的value值
@Max(value=值)
和@Min要求一样
验证注解的元素值小于等于@Max指定的value值
@DecimalMin(value=值)
和@Min要求一样
验证注解的元素值大于等于@ DecimalMin指定的value值
@DecimalMax(value=值)
和@Min要求一样
验证注解的元素值小于等于@ DecimalMax指定的value值
@Digits(integer=整数位数, fraction=小数位数)
和@Min要求一样
验证注解的元素值的整数位数和小数位数上限
@Size(min=下限, max=上限)
字符串、Collection、Map、数组等
验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小
@Past
java.util.Date,java.util.Calendar;Joda Time类库的日期类型
验证注解的元素值(日期类型)比当前时间早
@Future
与@Past要求一样
验证注解的元素值(日期类型)比当前时间晚
@Length(min=下限, max=上限)
CharSequence子类型
验证注解的元素值长度在min和max区间内
@Range(min=最小值, max=最大值)
BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型
验证注解的元素值在最小值和最大值之间
@Email(regexp=正则表达式,flag=标志的模式)
CharSequence子类型(如String)
验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式
@Pattern(regexp=正则表达式,flag=标志的模式)
String,任何CharSequence的子类型
验证注解的元素值与指定的正则表达式匹配
@Valid
任何非原子类型
指定递归验证关联的对象;如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证
@CreditCardNumber
数字
对信用卡号进行一个大致的验证
@URL (protocol=,host,port)
网址
检查是否是一个有效的URL,如果提供了protocol,host等,则该URL还需满足提供的条件
3 开始使用 3.1 快速开始
创建模型 User
在需要校验的属性上添加合适的注解,方可校验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class User implements Serializable { private static final long serialVersionUID = -7242397021777229674L ; @NotEmpty private String name; @Min(1) private Integer age; @Email private String email; @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}") private String phone; }
创建 Handler Method get 和 post
在请求参数前加 @Valid 或 @Validated 注解任一即可。 区别在于 @Valid 是 Java 的注解,@Validated 是 Spring 的注解,是对 @Valid 注解的增强,兼容 @Valid 注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @RequestMapping public class DemoController { @GetMapping public String get (@Valid User user) { return ObjUtils.toJsonStr(user); } @PostMapping public String post (@Validated @RequestBody User user) { return ObjUtils.toJsonStr(user); } }
统一异常处理,统一响应
当实体类中的注解校验不通过时,会抛出 BindException 异常,这里捕获到异常,获取到属性名和错误提示,组装返回。
1 2 3 4 5 6 7 8 9 10 11 12 @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BindException.class) public R bindExceptionHandler (BindException e) { log.debug("bindExcept: {}" , e.getMessage()); FieldError fieldError = e.getBindingResult().getFieldError(); assert fieldError != null ; return R.fail(String.format("%s %s" , fieldError.getField(), fieldError.getDefaultMessage())); } }
GET 请求 http://127.0.0.1:8080 ,响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "name must not be empty" }
3.2 自定义校验 有时候内置注解不能满足需求,我们可以自定义校验方式,参照文档 Creating custom constraints 一节。 如,前端传来一个值,我们需要验证值是否被包含在我们指定的值中。
创建自定义注解
strValues 指定字符串类型数组;intValues 指定数字类型数组;enumClass 指定一个枚举类。当指定的数组元素较多时,可以将其封装到枚举类中,简化编码。为了规范使用,这里定义了一个接口,枚举类必须实现这个接口才可以实现校验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {MatchAnyValidator.class}) @Repeatable(MatchAny.List.class) public @interface MatchAny { String message () default "not match any one" ; String[] strValues() default {}; int [] intValues() default {}; Class<? extends ValidateAble > enumClass() default EmptyValidateEnum.class; Class<?>[] groups() default {}; Class<? extends Payload >[] payload() default {}; @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented @interface List { MatchAny[] value(); } }
1 2 3 4 public interface ValidateAble { String getValidateValue () ; }
实现校验逻辑
这里需要实现 ConstraintValidator<MatchAny, Object> 接口,泛型的第一个参数是自定义注解类,第二个参数是可以校验的数据类型,因为我们自定义的注解可以校验 String 和 int 类型,所以这里使用了 Object。initialize 方法用来获取注解中的值,isValid 方法是校验逻辑,校验通过返回 true,否则返回 false。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 public class MatchAnyValidator implements ConstraintValidator <MatchAny, Object> { private String[] strValues; private int [] intValues; private Class<? extends ValidateAble > clazz; @Override public void initialize (MatchAny constraintAnnotation) { strValues = constraintAnnotation.strValues(); intValues = constraintAnnotation.intValues(); clazz = constraintAnnotation.enumClass(); } @Override public boolean isValid (Object value, ConstraintValidatorContext context) { if (Objects.isNull(value)) { return true ; } boolean pass; if (value instanceof Collection) { pass = ((Collection<?>) value).stream().allMatch(this ::validValue); } else if (value.getClass().isArray()) { pass = Arrays.stream(((Object[]) value)).allMatch(this ::validValue); } else { pass = validValue(value); } if (!pass) { context.disableDefaultConstraintViolation(); String valueStr = getValueStr(); context.buildConstraintViolationWithTemplate(String.format("must be one of [%s]" , valueStr)).addConstraintViolation(); return false ; } return true ; } private boolean validValue (Object value) { if (clazz != EmptyValidateEnum.class) { return Arrays.stream(clazz.getEnumConstants()).map(ValidateAble::getValidateValue).anyMatch(o -> Objects.equals(o, value)); } else if (value instanceof String) { return Arrays.asList(strValues).contains(value); } else if (value instanceof Integer) { return IntStream.of(intValues).anyMatch(i -> Objects.equals(i, value)); } else { return false ; } } private String getValueStr () { if (clazz.isEnum() && clazz != EmptyValidateEnum.class) { ValidateAble[] validateAbles = clazz.getEnumConstants(); return Arrays.stream(validateAbles).map(ValidateAble::getValidateValue).map(Object::toString).collect(Collectors.joining("," )); } else { if (ArrayUtils.isNotEmpty(strValues)) { return String.join("," , strValues); } else { return Arrays.stream(intValues).mapToObj(Objects::toString).collect(Collectors.joining("," )); } } } }
定义一个枚举类
枚举类要实现 ValidateAble 接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 @AllArgsConstructor public enum PermissionEnum implements ValidateAble { ADMIN("admin" ), STAFF("staff" ), BOSS("boss" ); private final String value; @Override public String getValidateValue () { return this .value; } }
使用自定义注解
在模型 User 中新增 permission 属性,并添加 @MatchAny 注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class User implements Serializable { private static final long serialVersionUID = -7242397021777229674L ; @NotEmpty private String name; @Min(1) private Integer age; @Email private String email; @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}") private String phone; @MatchAny(enumClass = PermissionEnum.class) private String permission; }
GET 请求 http://127.0.0.1:8080?name=bobby&permission=undefine ,响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "permission must be one of [admin,staff,boss]" }
3.3 PathVariable 数据校验 如果参数是在 uri 中该如何校验呢?
新增 Handler Method
在 DemoController 中新增一个 path 方法,其参数注解有 @PathVariable("age")。注意,需要在当前类上注解 @Validated 才生效,@Valid 无效,因此尽量使用 @Validated 注解。@Validated 注解在类型和注解在方法参数上并不冲突,如 post 方法中的 @Validated 只对 User 实体中的校验注解生效,而 DemoController 类上的 @Validated 只对 path 方法中的参数生效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Validated @RestController @RequestMapping public class DemoController { @GetMapping public String get (@Valid User user) { return ObjUtils.toJsonStr(user); } @PostMapping public String post (@Validated @RequestBody User user) { return ObjUtils.toJsonStr(user); } @GetMapping("/{age}") public String path (@PathVariable("age") @Min(1) Integer age) { return age.toString(); } }
增加异常捕获
方法中校验的参数若不是一个实体类,则用上述方式校验,此时抛出的异常为 ConstraintViolationException ,因此新增异常捕获。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BindException.class) public R bindExceptionHandler (BindException e) { log.debug("bindExcept: {}" , e.getMessage()); FieldError fieldError = e.getBindingResult().getFieldError(); assert fieldError != null ; return R.fail(String.format("%s %s" , fieldError.getField(), fieldError.getDefaultMessage())); } @ExceptionHandler(ConstraintViolationException.class) public R validationExceptionHandler (ConstraintViolationException e) { log.debug("validationException: {}" , e.getMessage()); ConstraintViolation<?> constraintViolation = e.getConstraintViolations().stream().findFirst().orElse(null ); assert constraintViolation != null ; String[] path = constraintViolation.getPropertyPath().toString().split("\\." ); String field = path[path.length - 1 ]; String message = constraintViolation.getMessage(); return R.fail(String.format("%s %s" , field, message)); } @ExceptionHandler(Exception.class) public R exceptionHandler (Exception e) { log.debug("unknown error: {}" , e.getMessage()); return R.fail(); } }
GET 请求 http://127.0.0.1:8080/0 ,响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "age must be greater than or equal to 1" }
3.4 嵌套校验 当模型中的属性嵌套了另一个模型,需要对嵌套的模型进行校验呢?
新增模型 Information
1 2 3 4 5 6 7 8 9 @Data public class Information implements Serializable { private static final long serialVersionUID = -3128793162502773246L ; @NotEmpty private String address; private String telPhone; }
在模型 User 中增加模型 Information 属性
注意,Information 属性上注解的是 @Valid,@Validated 不支持注解在这里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @GroupSequenceProvider(UserGroupSequenceProvider.class) public class User implements Serializable { private static final long serialVersionUID = -7242397021777229674L ; @Null(groups = {ValidAddGroup.class}) @NotNull(groups = {ValidUpdateGroup.class}) private Integer id; @NotEmpty(groups = {ValidAddGroup.class, ValidUpdateGroup.class}) private String name; @Min(1) private Integer age; @Email private String email; @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}") private String phone; @MatchAny(enumClass = PermissionEnum.class, groups = {ValidAddGroup.class, ValidUpdateGroup.class}) private String permission; @MatchAny(strValues = {"language", "math", "english"}, groups = {ValidLearningGroup.class}) private String learning; @MatchAny(strValues = {"screw", "brick", "coding"}, groups = {ValidWorkingGroup.class}) private String working; @Valid private Information information; }
POST 请求 http://127.0.0.1:8080/condition ,请求体:
1 2 3 4 5 6 7 { "age" : 20 , "name" : "xxx" , "information" : { } }
响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "information.address must not be empty" }
3.5 分组校验 有的时候同一个字段,在不同场合下的校验方式不同。例如:新增时我们需要校验 id 字段为空,但更新时需要校验 id 字段不为空,该如何做呢?
定义两个空接口
接口只需要定义,作为标记,无需任何方法实现.
1 2 3 public interface ValidAddGroup {}
1 2 3 public interface ValidUpdateGroup {}
在被校验字段的注解中加上分组
现在新增一个属性 id,并添加注解同时标记分组 @Null(groups = {ValidAddGroup.class}) 和 @NotNull(groups = {ValidUpdateGroup.class}) 。 表示,当指定了 ValidAddGroup 组时需要校验 @Null;当指定了 ValidUpdateGroup 组时需要校验 @NotNull。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class User implements Serializable { private static final long serialVersionUID = -7242397021777229674L ; @Null(groups = {ValidAddGroup.class}) @NotNull(groups = {ValidUpdateGroup.class}) private Integer id; @NotEmpty private String name; @Min(1) private Integer age; @Email private String email; @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}") private String phone; @MatchAny(enumClass = PermissionEnum.class) private String permission; }
新增两个 Handler Method
在 DemoController 中新增两个方法:add() 和 update()。在 add() 方法中我们指定了 ValidAddGroup 组,在 update() 中我们指定了 ValidUpdateGroup。 注意这里要使用 @Validated 注解,而 @Valid 不支持分组校验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Validated @RestController @RequestMapping public class DemoController { @GetMapping public R get (@Valid User user) { return R.data(user); } @PostMapping public R post (@Validated @RequestBody User user) { return R.data(user); } @GetMapping("/{age}") public R path (@PathVariable("age") @Min(1) Integer age) { return R.data(age); } @PostMapping("/add") public R add (@RequestBody @Validated({ValidAddGroup.class}) User user) { return R.data(user); } @PutMapping("/update") public R update (@RequestBody @Validated({ValidUpdateGroup.class}) User user) { return R.data(user); } }
POST 请求 http://127.0.0.1:8080/add ,请求体:
响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "id must be null" }
PUT 请求 http://127.0.0.1:8080/update ,请求体:
1 2 3 { "permission" : "sudo" }
响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "id must not be null" }
这时候有些长得帅的朋友可能要问了,model 中那些没有标记分组的注解还生效吗? 我们来试一试,当给 update() 方法请求体中给出 id 的值: PUT 请求 http://127.0.0.1:8080/update ,请求体:
1 2 3 4 { "id" : 1 , "permission" : "sudo" }
响应体:
1 2 3 4 5 6 7 8 9 10 11 12 13 { "code" : 0 , "data" : { "id" : 1 , "name" : null , "age" : null , "email" : null , "phone" : null , "permission" : "sudo" } , "success" : true , "message" : "success" }
我们可以看到成功响应了,但我们明明是有校验 name 属性 @NotEmpty 的,而且请求体中的 permission 属性值也不对,却还是通过了校验。这就说明,当我们在方法参数中指定了校验分组时,只有符合分组标记的校验才会生效,不符合或者没标记的一律不生效,因此就需要给 name 和 permission 注解上指定校验分组。但 name 和 permission 在新增和更新时都需要校验,此时可以同时添加 ValidAddGroup 和 ValidUpdateGroup 两种标记,因为校验注解中的 groups() 属性值是 一个数组 Class<?>[],@Validated 注解中的 value() 属性值同样也是一个数组 Class<?>[] ,因此也可以指定多个分组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class User implements Serializable { private static final long serialVersionUID = -7242397021777229674L ; @Null(groups = {ValidAddGroup.class}) @NotNull(groups = {ValidUpdateGroup.class}) private Integer id; @NotEmpty(groups = {ValidAddGroup.class,ValidUpdateGroup.class}) private String name; @Min(1) private Integer age; @Email private String email; @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}") private String phone; @MatchAny(enumClass = PermissionEnum.class,groups = {ValidAddGroup.class,ValidUpdateGroup.class}) private String permission; }
PUT 请求 http://127.0.0.1:8080/update ,请求体:
1 2 3 4 { "id" : 1 , "permission" : "sudo" }
响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "permission must be one of [admin,staff,boss]" }
3.6 逻辑校验 上面的校验方式仍然比较简单,当我们的需要自定义校验逻辑又需要怎么做呢?hibernate-validator 为我们提供了一个接口 DefaultGroupSequenceProvider,这个接口不是 JSR-303标准的,没有默认实现类。 例如,我们需要判断模型 User 中的 age 属性值,当 age <= 22 时,校验 learning 属性值,否则校验 working 属性值。其实,核心思想就是动态指定校验分组。
User 模型中新增属性 learning 和 working
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @GroupSequenceProvider(UserGroupSequenceProvider.class) public class User implements Serializable { private static final long serialVersionUID = -7242397021777229674L ; @Null(groups = {ValidAddGroup.class}) @NotNull(groups = {ValidUpdateGroup.class}) private Integer id; @NotEmpty(groups = {ValidAddGroup.class, ValidUpdateGroup.class}) private String name; @Min(1) private Integer age; @Email private String email; @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}") private String phone; @MatchAny(enumClass = PermissionEnum.class, groups = {ValidAddGroup.class, ValidUpdateGroup.class}) private String permission; @MatchAny(strValues = {"language", "math", "english"}, groups = {ValidLearningGroup.class}) private String learning; @MatchAny(strValues = {"screw", "brick", "coding"}, groups = {ValidWorkingGroup.class}) private String working; }
定义两个校验组 ValidLearningGroup 和 ValidWorkingGroup
1 2 public interface ValidLearningGroup {}
1 2 public interface ValidWorkingGroup {}
定义一个 DefaultGroupSequenceProvider 的实现
在模型 User 中加上 @GroupSequenceProvider(UserGroupSequenceProvider.class)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class UserGroupSequenceProvider implements DefaultGroupSequenceProvider <User> { @Override public List<Class<?>> getValidationGroups(User user) { List<Class<?>> defaultGroupSequence = new ArrayList <>(); defaultGroupSequence.add(User.class); if (Objects.nonNull(user)) { Integer age = user.getAge(); if (age <= 22 ) { defaultGroupSequence.add(ValidLearningGroup.class); } else { defaultGroupSequence.add(ValidWorkingGroup.class); } } return defaultGroupSequence; } }
新增一个 Handler Method condition
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Validated @RestController @RequestMapping public class DemoController { @GetMapping public R get (@Valid User user) { return R.data(user); } @PostMapping public R post (@Validated @RequestBody User user) { return R.data(user); } @GetMapping("/{age}") public R path (@PathVariable("age") @Min(1) Integer age) { return R.data(age); } @PostMapping("/add") public R add (@RequestBody @Validated({ValidAddGroup.class}) User user) { return R.data(user); } @PutMapping("/update") public R update (@RequestBody @Validated({ValidUpdateGroup.class}) User user) { return R.data(user); } @PostMapping("/condition") public R condition (@Validated @RequestBody User user) { return R.data(user); } }
POST 请求 http://127.0.0.1:8080/condition ,请求体:
1 2 3 4 5 { "age" : 50 , "learning" : "xxx" , "working" : "xxx" }
响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "working must be one of [screw,brick,coding]" }
这个时候那些长得帅的朋友可能又会问了,其他属性值还会校验吗? 我们来实测一下:
POST 请求 http://127.0.0.1:8080/condition ,请求体:
1 2 3 4 5 { "age" : 20 , "name" : "" , "email" : "xxx" }
响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "email must be a well-formed email address" }
我们可以看到,虽然 age 有值,但没有给 learning 属性,因此不会去校验。而 name 属性值虽然不符合,但由于其注解上有标记分组,而我们 handler 方法中没有指定分组(其实是默认分组),因此也不会校验。剩下的 email 属性,由于其注解上没有标记分组,即为默认分组,因此执行校验。 综上,对于分组校验,我们可以总结以下2点:
当 handler 中没有指定校验分组(即默认分组)时,模型中的没有指定分组(即默认分组)的属性,以及 DefaultGroupSequenceProvider 指定的分组(包含了默认分组)才会被校验;
当 handler 中指定了校验分组时,此时仅指定的分组才会被校验。
因此,当需要分组校验时,需要格外注意。
3.7 校验排序 若有多个校验分组时,默认校验是无序的。
新增一个 Handler Method sequence,此时并未指定任何分组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Validated @RestController @RequestMapping public class DemoController { @GetMapping public R get (@Valid User user) { return R.data(user); } @PostMapping public R post (@Validated @RequestBody User user) { return R.data(user); } @GetMapping("/{age}") public R path (@PathVariable("age") @Min(1) Integer age) { return R.data(age); } @PostMapping("/add") public R add (@RequestBody @Validated({ValidAddGroup.class}) User user) { return R.data(user); } @PutMapping("/update") public R update (@RequestBody @Validated({ValidUpdateGroup.class}) User user) { return R.data(user); } @PostMapping("/condition") public R condition (@Validated @RequestBody User user) { return R.data(user); } @GetMapping("/sequence") public R sequence (@Validated User user) { return R.data(user); } }
GET 请求 http://127.0.0.1:8080/sequence?email=xxx&phone=123
响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "phone must match \"[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}\"" }
可以看到,虽然在模型 User 中 email 定义在前,请求参数也是 email 在前,但还是先校验的 phone。 如果我们需要指定分组的校验顺序,先校验 email 呢,可以使用 @GroupSequence 注解,这是 JSR-303 提供的。
新增两个分组 EmailSeq 和 PhoneSeq
1 2 public interface EmailSeq {}
1 2 public interface PhoneSeq {}
定义一个分组序列
当指定校验分组为 UserSequence 时,会依次按照 @GroupSequence 指定的顺序执行校验。
1 2 3 @GroupSequence({EmailSeq.class,PhoneSeq.class}) public interface UserSequence {}
指定模型 User 中的 email 和 phone 属性的校验分组,并给 Handler Method sequence 增加 UserSequence 校验组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @GroupSequenceProvider(UserGroupSequenceProvider.class) public class User implements Serializable { private static final long serialVersionUID = -7242397021777229674L ; @Null(groups = {ValidAddGroup.class}) @NotNull(groups = {ValidUpdateGroup.class}) private Integer id; @NotEmpty(groups = {ValidAddGroup.class, ValidUpdateGroup.class}) private String name; @Min(1) private Integer age; @Email(groups = {EmailSeq.class}) private String email; @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}", groups = {PhoneSeq.class}) private String phone; @MatchAny(enumClass = PermissionEnum.class, groups = {ValidAddGroup.class, ValidUpdateGroup.class}) private String permission; @MatchAny(strValues = {"language", "math", "english"}, groups = {ValidLearningGroup.class}) private String learning; @MatchAny(strValues = {"screw", "brick", "coding"}, groups = {ValidWorkingGroup.class}) private String working; @Valid private Information information; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Validated @RestController @RequestMapping public class DemoController { @GetMapping public R get (@Valid User user) { return R.data(user); } @PostMapping public R post (@Validated @RequestBody User user) { return R.data(user); } @GetMapping("/{age}") public R path (@PathVariable("age") @Min(1) Integer age) { return R.data(age); } @PostMapping("/add") public R add (@RequestBody @Validated({ValidAddGroup.class}) User user) { return R.data(user); } @PutMapping("/update") public R update (@RequestBody @Validated({ValidUpdateGroup.class}) User user) { return R.data(user); } @PostMapping("/condition") public R condition (@Validated @RequestBody User user) { return R.data(user); } @GetMapping("/sequence") public R sequence (@Validated(UserSequence.class) User user) { return R.data(user); } }
GET 请求 http://127.0.0.1:8080/sequence?phone=123&email=xxx
响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "email must be a well-formed email address" }
可以看到,email 先被校验。
4 扩展 4.1 接口参数多态+参数校验 若有一个 Handler Method ,它的参数可能有多种对象,每种对象的校验方式又不相同,但是参数只定义一个对象接收,该如何做呢? 有两种方式实现:
方法一:将所有的子类属性定义在一个对象中,给属性校验添加分组,然后结合逻辑校验来动态指定不同的校验分组。这种方式的缺点在于,将所有子类对象属性揉在一起可能会很多,再添加上一堆校验分组,校验逻辑就很不直观,不便于维护。
方法二:实现方法请求参数的多态。定义一个父类,让所有子类都继承它,然后根据一个属性值来判断反序列化为哪种子类,然后在子类中完成各自的属性校验。这种方式就非常直观。
这里我将介绍方法二的实现步骤。
定义一个父类 Person,两个子类 Teacher 和 Student
父类中重写了子类中的所有方法,主要是为了能够不用强转为子类也可以获取子类的值,当然你可以不实现这些方法,使用的时候强转为目标类型即可。说明 :注解 @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true, defaultImpl = Person.class) 其中 property = "type" 表示判断的属性为 type,注意 defaultImpl = Person.class,这个如果不指定会反序列化失败,直接抛异常。而指定后表示当 type 不存在或者不是给定的值时,默认转为 Person 类型,然后就可以校验对象中的 type 属性值,@NotEmpty 和 @MatchAny 会生效。 注解 @JsonSubTypes 中的属性表示当 type值为 teacher 时实例化为 Teacher 类型,当type值为 student 时实例化为 Student 类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true, defaultImpl = Person.class) @JsonSubTypes({ @JsonSubTypes.Type(value = Teacher.class, name = "teacher"), @JsonSubTypes.Type(value = Student.class, name = "student") }) public class Person { @NotEmpty @MatchAny(strValues = {"teacher", "student"}) private String type; public Integer getStuId () { return null ; } public Integer getTeaId () { return null ; } public String getName () { return null ; } public Integer getAge () { return null ; } public String getClassNo () { return null ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @EqualsAndHashCode(callSuper = true) @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class Teacher extends Person { private Integer teaId; private String name; @Min(28) private Integer age; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @EqualsAndHashCode(callSuper = true) @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class Student extends Person { private Integer stuId; private String name; @Min(7) private Integer age; private String classNo; }
新增一个 Handler Method extend
实测,GET 和 POST 请求都可以完成类型映射,但只有为 POST 请求时才会校验子类中的属性。
1 2 3 4 5 6 7 8 9 @RestController @RequestMapping("/extend") public class ExtendController { @PostMapping public R extend (@RequestBody @Validated Person person) { return R.data(person); } }
POST 请求 http://127.0.0.1:8080/extend ,请求体:
1 2 3 4 { "type" : "teacher" , "age" : 15 }
响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "age must be greater than or equal to 28" }
4.2 请求参数解析+参数校验 最近在把 Node.js 项目重构为 Java 项目,其中一个接口为 GET https://host:port/uri?list=["aa","bb"],Node.js 的框架可以将 list 参数解析为数组类型;而在 Java 中,即使将 list 参数定义为集合或数组类型,Srpingmvc 框架默认还是会把 ["aa","bb"] 整个解析为集合中的一个元素。参数未能以预期的方式接收,亦无法通过参数校验。 我们知道 Springmvc 可以将 GET https://host:port/uri?list=aa,bb 形式的参数解析为集合或数组,但 Node.js 的框架无法解析这种传参方式。因为是重构项目,需要兼容 Node.js ,故不能修改传参方式。而一时间没有找到可以让 Springmvc 正确解析参数的方法,因此决定实现自定义参数解析器。
定义参数解析注解
用来标记需要解析的参数。
1 2 3 4 @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface ArrayResolver {}
实现 HandlerMethodArgumentResolver 接口,自定义参数解析器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public class ArrayHandlerMethodArgumentResolver extends AbstractCustomizeResolver { @Override public boolean supportsParameter (MethodParameter parameter) { return parameter.hasParameterAnnotation(ArrayResolver.class); } @Override public Object resolveArgument (MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { Object obj = BeanUtils.instantiateClass(parameter.getParameterType()); BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(obj); Iterator<String> parameterNames = webRequest.getParameterNames(); while (parameterNames.hasNext()) { String name = parameterNames.next(); String camelName = underLineToCamel(name); Class<?> propertyType = wrapper.getPropertyType(camelName); if (Objects.isNull(propertyType)) { continue ; } Object o = webRequest.getParameter(name); if (Objects.nonNull(o)) { if (propertyType.isArray()) { wrapper.setPropertyValue(camelName, value2Array(o)); } else if (Collection.class.isAssignableFrom(propertyType)) { wrapper.setPropertyValue(camelName, array2Collection(propertyType, value2Array(o))); } else { wrapper.setPropertyValue(camelName, o); } } } valid(parameter, mavContainer, webRequest, binderFactory, obj); return obj; } private Object[] value2Array(Object o) { assert o != null ; if (StringUtils.containsAny(o.toString(), "[" , "]" )) { return JSON.parseArray(o.toString()).toArray(); } else { return JSON.parseArray(Arrays.toString(o.toString().split("," ))).toArray(); } } private Collection<Object> array2Collection (Class<?> propertyType, Object[] array) { Collection<Object> collection = CollectionFactory.createCollection(propertyType, array.length); Collections.addAll(collection, array); return collection; } }
将参数解析器添加到 Spring 容器中
1 2 3 4 5 6 7 @Configuration public class SpringMvcConfig implements WebMvcConfigurer { @Override public void addArgumentResolvers (List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new ArrayHandlerMethodArgumentResolver ()); } }
在模型 Teacher 中新增 lessons 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @EqualsAndHashCode(callSuper = true) @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class Teacher extends Person { private Integer teaId; private String name; @Min(28) private Integer age; @MatchAny(enumClass = LessonEnum.class) private List<String> lessons; }
新增 Handler Method arrayResolve
参数前标记 @ArrayResolver 注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @RequestMapping("/extend") public class ExtendController { @PostMapping public R extend (@RequestBody @Validated Person person) { return R.data(person); } @GetMapping("/arrayResolve") public R arrayResolve (@ArrayResolver @Validated Teacher teacher) { return R.data(teacher); } }
GET 请求 http://127.0.0.1:8080/extend/arrayResolve?lessons=[“math”,”xxx”]
响应体:
1 2 3 4 5 6 { "code" : 500 , "data" : null , "success" : false , "message" : "lessons must be one of [language,math,english]" }
可以看到,参数被正确解析且被校验。
Tip:本文完整示例代码已上传至 Gitee