一、先认识两个"阵营"
这些注解不是同一家的,来自两个不同的"势力":
| 阵营 | 来源 | 角色 | 包路径 |
|---|---|---|---|
| 校验注解 | 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 校验简单参数 |