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 校验简单参数
相关推荐
CHANCE V18 分钟前
集合排序和流排序
java
IT_陈寒19 分钟前
被Java的final坑惨了,这些细节你可能也忽略了
前端·人工智能·后端
16月6日-晴39 分钟前
Java面向对象进阶—多态
java·开发语言
Zadig40 分钟前
企业 Agent 总烂在 Demo 里?Zadig 工作流 AI 任务给了一条路
后端·devops
Zadig1 小时前
告别"人肉扛雷":Zadig 用 AI 接管发布前最脏最累的 15 分钟
后端·aiops
Nturmoils1 小时前
向量数据库不该成为新孤岛:KingbaseES 多模融合架构如何减少数据搬运
后端
一座古城1 小时前
Claude Code 架构源码解析:AI 应用开发的范式跃迁
前端·后端
Nturmoils1 小时前
用蓝耘元生代做 GitHub 热榜解读:Dify Chatflow 接入和真实项目分析
后端
K哥爬虫1 小时前
【JS 逆向百例】Vaptcha V4 手势验证码逆向分析
前端·后端
qq_185198692 小时前
SpringBoot-五-AOT
java·spring boot·后端