引入依赖
如果 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 {};
}
-
实现 ConstraintValidator 接口编写约束校验器:
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 进行参数校验了!