Spring @Validated和Validation注解 校验机制完全指南

一、先认识两个"阵营"

这些注解不是同一家的,来自两个不同的"势力":

阵营 来源 角色 包路径
校验注解 Jakarta Bean Validation(JSR 380) 规则制定者 jakarta.validation.constraints
@Validated Spring Framework 总指挥 org.springframework.validation.annotation

二、打个比方:考试系统

复制代码
@NotNull  →  试卷上的题目(规则)
@Pattern  →  答题格式要求(规则)
@Validated → 监考老师(决定何时开始、考哪套卷子)

试卷上的题写得再好,没有监考老师喊"开始",谁也不会动笔。


三、@Validated 的三种用法

用法一:参数上,不加分组 → 全体校验

复制代码
@PostMapping
public R<Void> add(@Validated @RequestBody SupplierInvoiceBo bo) {
    // 触发 bo 中所有校验注解,不管有没有 groups
}

监考老师:"所有人!开始考试!"

用法二:参数上,指定分组 → 只校验该组

复制代码
@PostMapping
public R<Void> add(@Validated(AddGroup.class) @RequestBody SupplierInvoiceBo bo) {
    // 只触发 groups = AddGroup.class 的注解
}

监考老师:"一组的同学!开始考试!"

⚠️ 一旦指定了分组,无分组的校验注解会被跳过:

复制代码
@NotNull(message = "供应商ID不能为空", groups = {AddGroup.class})   // ✅ 被触发
@Pattern(regexp = "^[0-9A-Z]{18}$")                              // ❌ 被跳过!没有 groups!

用法三:类上 → 管方法参数本身

复制代码
@Validated  // 放在类上
@RestController
public class Controller {

    @DeleteMapping("/{id}")
    public R<Void> remove(@PathVariable @NotNull(message = "ID不能为空") Long id) {
        // ✅ 类上的 @Validated 让 @NotNull 在方法参数上生效
    }
}

三种用法总结

放置位置 校验范围 典型场景
类上 方法参数上直接写的校验注解 @PathVariable @NotNull Long id
参数上(无分组) 该对象内部所有校验注解 @RequestBody Bo 全体校验
参数上(有分组) 该对象内部指定分组的校验 @Validated(AddGroup.class) 只校验新增

一句话:类上的管"表面",参数上的管"里面"。


四、分组校验(Group)详解

什么是分组?

就是一个空的标记接口,纯粹用来"贴标签":

复制代码
public interface AddGroup {}   // 一班
public interface EditGroup {}  // 二班

为什么需要分组?

同一个 Bo 被多个接口复用,但校验规则不同:

复制代码
public class SupplierInvoiceBo {
    @NotNull(message = "开票信息ID不能为空", groups = {EditGroup.class})   // 只有编辑时需要
    private Long invoiceId;

    @NotNull(message = "供应商ID不能为空", groups = {AddGroup.class})      // 只有新增时需要
    private Long supplierId;

    @NotBlank(message = "抬头不能为空", groups = {AddGroup.class, EditGroup.class}) // 都需要
    private String invoiceTitle;
}
接口 触发校验
新增 @Validated(AddGroup.class) supplierId、invoiceTitle
编辑 @Validated(EditGroup.class) invoiceId、invoiceTitle

分组校验生效规则(核心!)

Controller 写法 触发的校验 跳过的校验
@Validated 所有注解(有 groups 和无 groups 的)
@Validated(AddGroup.class) 只触发 groups = AddGroup.class 无 groups 的、其他 groups 的

⚠️ 一旦指定了分组,无分组的校验注解会被跳过。这是最容易踩的坑。


五、常用校验注解速查表

注解 作用 适用类型
@NotNull 不能为 null 任何类型
@NotBlank 不能为 null,且去掉首尾空格后长度 > 0 String
@NotEmpty 不能为 null,且 size > 0 String、Collection、Map
@Size(min, max) 长度范围 String、Collection
@Min / @Max 最小值/最大值 数值类型
@Pattern(regexp) 正则匹配 String
@Email 邮箱格式 String
@Positive 必须为正数 数值类型

@NotNull vs @NotBlank vs @NotEmpty 区别

注解 null "" " " "abc"
@NotNull
@NotEmpty
@NotBlank

六、@Valid vs @Validated

区别点 @Valid @Validated
来源 Jakarta Spring
分组功能 ❌ 不支持 ✅ 支持
嵌套校验 ✅ 支持 ❌ 不支持
用在类上 ❌ 不能 ✅ 可以
用在参数上 ✅ 可以 ✅ 可以

嵌套校验:

复制代码
public class OrderBo {
    @Valid  // ← 必须加这个,否则 UserBo 内部的校验不会触发
    private UserBo user;
}

七、完整实战示例

复制代码
// 1. 分组接口
public interface AddGroup {}
public interface EditGroup {}

// 2. Bo
@Data
public class SupplierInvoiceBo {

    @NotNull(message = "开票信息ID不能为空", groups = {EditGroup.class})
    private Long invoiceId;

    @NotNull(message = "供应商ID不能为空", groups = {AddGroup.class})
    private Long supplierId;

    @NotBlank(message = "开票抬头不能为空", groups = {AddGroup.class, EditGroup.class})
    private String invoiceTitle;

    @Pattern(regexp = "^$|^[0-9A-Z]{18}$", message = "信用编码格式不正确",
             groups = {AddGroup.class, EditGroup.class})  // ← 别忘了加 groups!
    private String taxNo;
}

// 3. Controller
@Validated  // 管方法参数
@RestController
@RequestMapping("/supplier/supplierInvoice")
public class SupplierInvoiceController {

    @GetMapping("/list")
    public R<List<Vo>> list(SupplierInvoiceBo bo) { }  // 不校验,可传可不传

    @PostMapping
    public R<Void> add(@Validated(AddGroup.class) @RequestBody SupplierInvoiceBo bo) { }

    @PutMapping
    public R<Void> edit(@Validated(EditGroup.class) @RequestBody SupplierInvoiceBo bo) { }

    @DeleteMapping("/{invoiceId}")
    public R<Void> remove(@PathVariable @NotNull(message = "ID不能为空") Long invoiceId) { }
}

八、常见踩坑清单

说明
@Pattern 没写 groups 指定分组校验时被跳过,格式校验不生效
@NotBlank 用在 Long 上 @NotBlank 只能用于 String,Long 用 @NotNull
GET 请求用 @RequestBody GET 不带 body,浏览器不支持,用 URL 参数绑定
嵌套对象忘加 @Valid 内部对象的校验注解不触发
唯一性校验放 Bo 层 查数据库的逻辑放 Service 层,Bo 只做格式校验

九、自定义校验注解(进阶)

复制代码
// 1. 定义注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
    String message() default "手机号格式不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 2. 实现校验器
public class PhoneValidator implements ConstraintValidator<Phone, String> {
    private static final Pattern PATTERN = Pattern.compile("^1[3-9]\\d{9}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null || value.isEmpty()) return true;
        return PATTERN.matcher(value).matches();
    }
}

// 3. 使用
@Phone(groups = {AddGroup.class, EditGroup.class})
private String phone;

十、最佳实践总结

原则 说明
格式校验放 Bo 层 @NotNull@Pattern
业务校验放 Service 层 唯一性、存在性、状态校验等需要查数据库的
分组清晰 每个校验注解都指定 groups
查询接口不校验 list 不加 @Validated,支持可选参数
类上放 @Validated 配合 @PathVariable @NotNull 校验简单参数
相关推荐
唐青枫1 小时前
Java RxJava 实战指南:从 Observable、Flowable 到线程切换和背压处理
java
卷无止境1 小时前
Python进程池那些事儿:从原理到实战
后端·python
卷无止境1 小时前
Python 线程池全解析:从原理到实战
后端
北冥you鱼1 小时前
Go语言四则运算实战:从基础类型到big包的深度解析
开发语言·后端·golang
凤山老林1 小时前
SpringBoot + Configuration2 实现配置的实时双向更新
java·spring boot·后端
艾莉丝努力练剑2 小时前
【MYSQL】MYSQL学习的一大重点:事务(上)- 原子性与持久性
android·数据库·学习·mysql·面试
2601_963869953 小时前
【计算机毕业设计】基于 Spring Boot+Vue的手工体验馆管理系统的设计与实现
java·spring boot·后端
曹牧3 小时前
Java:BeanListHandler
java·数据库·oracle
ITenderL5 小时前
MySQL联合索引遇到范围查询
数据库·索引