Spring 参数校验,别只会加个 @Valid
起因
接口参数校验,最开始我的做法就是 Controller 方法参数上加个 @Valid,实体类字段上标几个 @NotNull,报错了就返回 400。能用,但用久了发现几个问题:报错信息是英文的、嵌套对象不校验、分组场景没法处理、自定义规则写起来很别扭。
这篇把校验这块我实际用过的东西记一下。
基础的那套
依赖先确认有:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
字段上标约束:
public class OrderCreateReq {
@NotBlank(message = "订单号不能为空")
private String orderNo;
@NotNull(message = "金额不能为空")
@DecimalMin(value = "0.01", message = "金额必须大于0")
private BigDecimal amount;
@Size(max = 200, message = "备注不能超过200字")
private String remark;
}
Controller 上加 @Valid 或者 @Validated。这两个的区别:@Valid 是 JSR 标准注解,@Validated 是 Spring 的,支持分组。只用基础校验的话,两个都行,我一般用 @Validated,后面加分组方便。
报错信息得改
默认报错返回的是一大串英文,前端拿到没法直接用。得在全局异常处理器里把 MethodArgumentNotValidException 接住,提取出字段和 message 重新组装:
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidException(
MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(err ->
errors.put(err.getField(), err.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
message 里写中文是我现在的习惯,省得前端再翻译一遍。代价是代码里全是中文字符串,有人不喜欢,看团队规范。
嵌套对象,得一层层标
这个坑前面提过,单独展开说。
public class OrderCreateReq {
@Valid
@NotNull(message = "用户信息不能为空")
private UserInfo user;
}
public class UserInfo {
@NotBlank(message = "用户名不能为空")
private String name;
}
OrderCreateReq 里的 user 字段上如果只标 @NotNull 不标 @Valid,UserInfo 里面的 name 是不会被校验的。嵌套几层就得标几层,漏一层就断链。集合类型的字段也一样,List<@Valid UserInfo> 或者字段上标 @Valid 都行,看具体写法。
分组,解决“新增和修改规则不一样”
同一个实体,新增的时候 ID 为空,修改的时候 ID 必填。用分组:
public interface Create {}
public interface Update {}
public class UserReq {
@NotNull(groups = Update.class, message = "ID不能为空")
private Long id;
@NotBlank(groups = {Create.class, Update.class}, message = "用户名不能为空")
private String name;
}
Controller 里指定用哪组:
@PostMapping("/create")
public Result create(@Validated(Create.class) @RequestBody UserReq req) { }
@PutMapping("/update")
public Result update(@Validated(Update.class) @RequestBody UserReq req) { }
注意这里必须用 @Validated,@Valid 不支持指定分组。用了分组之后,没标任何组的约束默认不生效,得显式指定 Default.class 或者把所有约束都归到组里。这个容易漏,改成分组之后发现某些字段突然不校验了,多半就是这个原因。
自定义校验注解
内置的注解覆盖不到的业务规则,得自己写。
比如校验手机号格式:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
校验器实现 ConstraintValidator:
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern P = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (value == null) return true; // 空值交给 @NotBlank 管
return P.matcher(value).matches();
}
}
这里有个约定:自定义校验器对 null 一般返回 true,空值校验交给 @NotNull/@NotBlank,各管各的。如果校验器里对 null 返回 false,和 @NotBlank 叠加的时候报错信息会乱。
方法级别的校验,@Validated 标在类上
上面说的都是 Controller 参数校验,Spring MVC 自动触发。如果要在 Service 层的方法参数上校验,得在类上标 @Validated,方法参数上标约束注解:
@Service
@Validated
public class UserService {
public void update(@NotNull Long id, @NotBlank String name) { }
}
这时候校验不通过抛的是 ConstraintViolationException,不是 MethodArgumentNotValidException,全局异常处理器里得单独接一个。
这个特性我用得不多,因为 Service 层的参数一般 Controller 已经校验过了。但有些内部调用不经过 Controller,加上能兜个底。副作用是 @Validated 标在类上会生成代理,跟事务、异步那些代理叠在一起,偶尔出一些奇怪的问题,不确定的话别乱标。
小结
校验这块,基础用法几分钟就能上手,麻烦的是嵌套、分组、自定义规则这几块。我现在的习惯是:实体类字段全标约束,Controller 用 @Validated 指定分组,全局异常处理器统一转成字段到中文消息的 Map 返回,自定义校验器只写确实内置覆盖不到的规则。
- 点赞
- 收藏
- 关注作者
评论(0)