引入依赖
如果 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>
对于 Web 服务来说,为防止非法参数对业务造成影响,在 Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:
POST、PUT 请求,使用 requestBody 传递参数;
GET 请求,使用 requestParam/PathVariable 传递参数。
requestBody 参数校验
POST、PUT 请求一般会使用 requestBody 传递参数,这种情况下,后端使用 DTO 对象进行接收。只要给 DTO 对象加上 @Validated 注解就能实现自动参数校验。如果校验失败,会抛出 MethodArgumentNotValidException 异常,Spring 默认会将其转为 400(Bad Request)请求。
DTO 表示数据传输对象(Data Transfer Object),用于服务器和客户端之间交互传输使用的。在 spring-web 项目中可以表示用于接收请求参数的Bean对象。
这种情况下,使用 @Valid 和 @Validated 都可以。
requestParam/PathVariable 参数校验
GET 请求一般会使用 requestParam/PathVariable 传参。如果参数比较多(比如超过6个),还是推荐使用 DTO 对象接收。否则,推荐将一个个参数平铺到方法入参中。在这种情况下,必须在 Controller 类上标注 @Validated 注解,并在入参上声明约束注解(如 @Min 等)。如果校验失败,会抛出 ConstraintViolationException 异常。
统一异常处理
// 当请求参数标识注解 @RequestBody else if (e instanceof MethodArgumentNotValidException) { BindingResult bindingResult = ((MethodArgumentNotValidException) e).getBindingResult(); String msg = ""; if (bindingResult.hasErrors()) { List<ObjectError> errors = bindingResult.getAllErrors(); errors.forEach(p -> { FieldError fieldError = (FieldError) p; buffer.append(fieldError.getDefaultMessage()).append(","); }); if(buffer.length() > 1) { msg = buffer.substring(0, buffer.lastIndexOf(",")); } } result.setCode(ResultCode.FAIL).setMsg(msg); } // @Validated一般情况 else if (e instanceof BindException) { BindingResult bindingResult = ((BindException) e).getBindingResult(); String msg = ""; if (bindingResult.hasErrors()) { List<ObjectError> errors = bindingResult.getAllErrors(); errors.forEach(p -> { FieldError fieldError = (FieldError) p; buffer.append(fieldError.getDefaultMessage()).append(","); }); if(buffer.length() > 1) { msg = buffer.substring(0, buffer.lastIndexOf(",")); } } result.setCode(ResultCode.FAIL).setMsg(msg); } // @PathVariable 、@RequestParam 注解的参数 else if (e instanceof ConstraintViolationException) { List<String> errors = ((ConstraintViolationException) e).getConstraintViolations() .stream() .map(ConstraintViolation::getMessage) .collect(Collectors.toList()); String msg = ""; errors.forEach(p -> { buffer.append(p).append(","); }); if(buffer.length() > 1) { msg = buffer.substring(0, buffer.lastIndexOf(",")); } result.setCode(ResultCode.FAIL).setMsg(msg); }
嵌套校验
前面的示例中,DTO 类里面的字段都是基本数据类型和 String 类型。但是实际场景中,有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验。比如,上面保存User信息的时候同时还带有 Job 信息。需要注意的是,此时 DTO 类的对应字段必须标记 @Valid 注解。
public class UserDTO { @Min(value = 10000000000000000L, groups = Update.class) private Long userId;
@NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String userName;
@NotNull(groups = {Save.class, Update.class}) @Valid private Job job;
@Data public static class Job {
@Min(value = 1, groups = Update.class) private Long jobId;
@NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String jobName;
@NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String position; }
集合校验
如果请求体直接传递了 JSON 数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用 java.util.Collection 下的 List 或者 Set 来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数。
包装 List 类型,并声明 @Valid 注解:
public class ValidationList<E> implements List<E> { @Delegate // @Delegate是lombok注解 @Valid // 一定要加@Valid注解 public List<E> list = new ArrayList<>();
// 一定要记得重写toString方法 @Override public String toString() { return list.toString(); } }
如果校验不通过,会抛出 NotReadablePropertyException,同样可以使用统一异常进行处理。
比如,我们需要一次性保存多个 User 对象,Controller 层的方法可以这么写:
@PostMapping("/saveList") public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) { // 校验通过,才会执行业务逻辑处理 return Result.ok(); }
自定义校验
业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。自定义 Spring Validation 非常简单,假设我们自定义加密 id(由数字或者 a-f 的字母组成,32-256 长度)校验,主要分为两步。
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {EncryptIdValidator.class}) public @interface EncryptId { // 默认错误消息 String message() default "加密id格式错误";
// 分组 Class<?>[] groups() default {};
// 负载 Class<? extends Payload>[] payload() default {}; }
public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> { private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");
@Override public boolean isValid(String value, ConstraintValidatorContext context) { // 不为null才进行校验 if (value != null) { Matcher matcher = PATTERN.matcher(value); return matcher.find(); } return true; } }
这样我们就可以使用 @EncryptId 进行参数校验了! |