- A+
在spring开发中,对入参进行校验是一种常见且必须的需求,下面对springboot中引入validation组件实现校验进行简单的分析一下。
Java API 规范 JSR303 定义了 Bean 校验的标准,但没提供实现,而常用的hibernate validation则是对这个规范的实现,提供了校验注解@Min、@Max等。
spring validation是对hibernate validation的二次封装,用于spirng中参数的校验。
若springboot版本低于2.3.时,spring-boot-starter-web会自动传入hibernate-validation依赖,若是版本高于2.3.,则需要手动引入hibernate-validation依赖。
示例如下图
对于web服务,为防止非法参数对业务造成影响,在controller层对http请求传递的参数进行校验分析。requestBody以json形式接收参数。
在http请求中,POST类型请求主要使用requestBody传递参数,后端使用*DTO对象接收参数,此时只须给DTO对象添加@Validated或@Valid注解,在DTO对象的字段上添加声明式约束注解,就能实现自动参数校验。其中Valid是java提供的javax.validation.Valid;,Validated是spring提供的org.springframework.validation.annotation.Validated;。
-
@Valid:可以用在方法、构造方法、方法参数、成员变量上;
-
@Validated:用在类型、方法、方法参数上,但不能用在成员变量上;
-
@Valid:没用分组功能;
-
@Validated:提供分组功能;
实例1
@PostMapping("add") public Object addUser(@RequestBody @Valid UserDTO param){ System.out.printf("save user id is %s", param.getUserId()); return param; } @PostMapping("update") public Object updateUser(@RequestBody @Validated UserDTO param){ System.out.printf("update user is %s", param.getUserId()); return param; }
@Data public class UserDTO { private Long userId; @NotNull @Length(min = 2, max = 10) private String userName; @NotNull @Length(min = 6, max = 20) private String account; @NotNull @Length(min = 6, max = 20) private String password; }
返回
在http请求中,GET类型请求主要使用requestParam/PathVariable传递参数,此时必须在controller类上标注@validated注解,在DTO对象的字段上添加声明式约束注解或在入参上添加声明式约束注解,完成参数校验。
实例2
返回
如果校验失败,会抛出MethodArgumentNotValidException或ConstraintViolationException异常,在实际工程中,通常通过统一异常处理来返回一个更友好的提示。
实例3
@RestControllerAdvice public class CommonExceptionHandler { @ExceptionHandler({MethodArgumentNotValidException.class}) public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { BindingResult bindingResult = ex.getBindingResult(); StringBuilder sb = new StringBuilder("校验失败,"); for (FieldError fieldError : bindingResult.getFieldErrors()) { sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", "); } String msg = sb.toString(); return ResponseData.fail(msg,400); } @ExceptionHandler({ConstraintViolationException.class}) public Object handleConstraintViolationException(ConstraintViolationException ex) { return ResponseData.fail(ex.getMessage(), 400); } }
返回
如果有多个接口需要使用同一个DTO对象来接收参数,而不同的接口的校验规则是不一样的,通常通过分组校验来实现。
实例4
@Data public class UserDTO { @NotNull(groups = Update.class) @Min(value = 1000L, groups = Update.class) private Long userId; @NotNull(groups = {Insert.class, Update.class}) @Length(min = 2, max = 10, groups = {Insert.class, Update.class}) private String userName; @NotNull(groups = {Insert.class, Update.class}) @Length(min = 6, max = 20, groups = {Insert.class, Update.class}) private String account; @NotNull(groups = {Insert.class, Update.class}) @Length(min = 6, max = 20, groups = {Insert.class, Update.class}) private String password; } public interface Insert { } public interface Update { }
@PostMapping("add3") public Object addUser3(@RequestBody @Validated(Insert.class) UserDTO param){ System.out.printf("save user id is %s", param.getUserId()); return param; } @PostMapping("update3") public Object updateUser3(@RequestBody @Validated(Update.class) UserDTO param){ System.out.printf("update user is %s", param.getUserId()); return param; }
返回
如果DTO对象里面的字段有基本数据类型,也出现了某个字段是一个对象,这时通过嵌套校验来实现。需要注意的是,此时DTO类的对应字段必须标记@Valid注解。
实例5
@Data public class UserDTO { @NotNull(groups = Update.class) @Min(value = 1000L, groups = Update.class) private Long userId; @NotNull(groups = {Insert.class, Update.class}) @Length(min = 2, max = 10, groups = {Insert.class, Update.class}) private String userName; @NotNull(groups = {Insert.class, Update.class}) @Length(min = 6, max = 20, groups = {Insert.class, Update.class}) private String account; @NotNull(groups = {Insert.class, Update.class}) @Length(min = 6, max = 20, groups = {Insert.class, Update.class}) private String password; @NotNull(groups = {Insert.class, Update.class}) @Valid private Job job; @Data public static class Job { @NotNull(groups = Update.class) @Min(value = 1, groups = Update.class) private Long jobId; @NotNull(groups = {Insert.class, Update.class}) @Length(min = 2, max = 10, groups = {Insert.class, Update.class}) private String jobName; @NotNull(groups = {Insert.class, Update.class}) @Length(min = 2, max = 10, groups = {Insert.class, Update.class}) private String position; } }
返回
如果请求体直接传递了json数组到后端,并希望对数组中的每一项都进行参数校验,此时使用java.util.Collection下的list或set来接收数据,参数校验并不会生效,需要使用自定义的list集合来实现。
实例6
public class ValidationList<E> implements List<E> { @Delegate @Valid public List<E> list = new ArrayList<>(); @Override public String toString() { return list.toString(); } public List<E> getList() { return list; } public void setList(List<E> list) { this.list = list; } }
@PostMapping("/addList") public Object addList(@RequestBody @Validated(Insert.class) ValidationList<UserDTO> userList) { return ResponseData.ok(userList); }
返回
如果出现一些特殊的业务需求,此时可以通过自定义校验来实现。假设自定义加密字段校验(由数字或者a-f的字母组成,32-256长度)。
实例7
@Data public class UserDTO implements Serializable { private static final long serialVersionUID = 1L; @NotNull(groups = Update.class) @Min(value = 1000L, groups = Update.class) private Long userId; @NotNull(groups = {Insert.class, Update.class}) @Length(min = 2, max = 10, groups = {Insert.class, Update.class}) private String userName; @NotNull(groups = {Insert.class, Update.class}) @Length(min = 6, max = 20, groups = {Insert.class, Update.class}) private String account; @Encrypt(groups = {Insert.class, Update.class}) @NotNull(groups = {Insert.class, Update.class}) @Length(min = 6, max = 20, groups = {Insert.class, Update.class}) private String password; @NotNull(groups = {Insert.class, Update.class}) @Valid private Job job; @Data public static class Job { @NotNull(groups = Update.class) @Min(value = 1, groups = Update.class) private Long jobId; @NotNull(groups = {Insert.class, Update.class}) @Length(min = 2, max = 10, groups = {Insert.class, Update.class}) private String jobName; @NotNull(groups = {Insert.class, Update.class}) @Length(min = 2, max = 10, groups = {Insert.class, Update.class}) private String position; } }
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {EncryptValidator.class}) public @interface Encrypt { String message() default "加密格式错误"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
public class EncryptValidator implements ConstraintValidator<Encrypt, String> { private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$"); @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (value != null) { Matcher matcher = PATTERN.matcher(value); return matcher.find(); } return true; } }
@PostMapping("update5") public Object updateUser(@RequestBody @Validated(Update.class) UserDTO param){ System.out.printf("save user id is %s", param.getUserId()); return param; }
返回
在spring-mvc中,RequestResponseBodyMethodProcessor是用于解析@RequestBody标注的参数以及处理@ResponseBody标注方法的返回值的。源码如下
实例8
自定义校验参数为指定的值。
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {SexValidator.class}) public @interface Sex { String message() default "须为指定值"; String[] strValues() default {}; int[] intValues() default {}; //使用指定枚举,使用属性命名code Class<?> enumValue() default Class.class; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
public class SexValidator implements ConstraintValidator<Sex, Object> { private String[] strValues; private int[] intValues; private Class<?> aClass; @Override public void initialize(Sex constraint) { strValues = constraint.strValues(); intValues = constraint.intValues(); aClass = constraint.enumValue(); } @SneakyThrows @Override public boolean isValid(Object value, ConstraintValidatorContext context) { if (null == value) { return true; } if (aClass.isEnum()) { Object[] objects = aClass.getEnumConstants(); for (Object obj : objects) { //此处方法getCode需要根据自己项目枚举的命名而变化 Method method = aClass.getDeclaredMethod("getCode"); String expectValue = String.valueOf(method.invoke(obj)); if (expectValue.equals(String.valueOf(value))) { return true; } } } else { if (value instanceof String) { for (String s : strValues) { if (s.equals(value)) { return true; } } } else if (value instanceof Integer) { for (Integer s : intValues) { if (s == value) { return true; } } } } return false; } }
@Data public class User implements Serializable { private static final long serialVersionUID = 1L; private Long userId; //@Sex(groups = {Update.class}, message = "须为指定值", intValues = {0,1}) @Sex(groups = {Update.class}, message = "须为指定值", enumValue = SexEnum.class) private Integer sex; @Sex(groups = {Update.class}, message = "须为指定值", strValues = {"男","女"}) private String sexStr; }
@PostMapping("update") public Object updateUser(@RequestBody @Validated(Update.class) User param){ System.out.printf("save user id is %s", param.getUserId()); return param; }
测试结果
可见可以对int类型、string类型、枚举类型进行指定校验。
参数校验还需不断的深入应用并理解其底层原理。