Spring 参数校验,别只会加个 @Valid

举报
yd_241780069 发表于 2026/11/02 22:57:30 2026/11/02
【摘要】 起因接口参数校验,最开始我的做法就是 Controller 方法参数上加个 @Valid,实体类字段上标几个 @NotNull,报错了就返回 400。能用,但用久了发现几个问题:报错信息是英文的、嵌套对象不校验、分组场景没法处理、自定义规则写起来很别扭。这篇把校验这块我实际用过的东西记一下。 基础的那套依赖先确认有:<dependency> <groupId>org.springfr...

起因

接口参数校验,最开始我的做法就是 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 返回,自定义校验器只写确实内置覆盖不到的规则。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。