第 48 章:参数校验
学习目标
- 用
@Valid/@Validated校验请求参数 - 学会常用校验注解
- 学会自定义校验
一、为什么需要参数校验
java
// ❌ 手动判空,代码冗长
if (user.getName() == null || user.getName().isEmpty()) { throw new IllegalArgumentException(); }
if (user.getAge() < 0 || user.getAge() > 150) { throw new IllegalArgumentException(); }解决:注解 + AOP 自动校验。
二、常用注解
java
@Data
public class UserDTO {
@NotNull(message = "ID 不能为空")
private Long id;
@NotBlank(message = "用户名不能为空") // 字符串非 null + 去除空格后长度 > 0
@Size(min = 3, max = 20, message = "用户名长度 3-20")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 0, message = "年龄不能小于 0")
@Max(value = 150, message = "年龄不能大于 150")
private Integer age;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误")
private String phone;
@Past(message = "生日必须过去") // 时间必须早于现在
private LocalDate birthday;
@Future(message = "预约时间必须未来")
private LocalDateTime appointmentTime;
}| 注解 | 适用 |
|---|---|
@NotNull | 任意类型,不能 null |
@NotBlank | 字符串,非空 + 去除空格后长度 > 0 |
@NotEmpty | 集合/字符串,非 null + 长度 > 0 |
@Size(min, max) | 字符串/集合/数组 |
@Min / @Max | 数字 |
@Email / @Pattern | 字符串 |
@Past / @Future | 时间 |
三、Controller 用法
java
@PostMapping("/users")
public Result<UserVO> create(@RequestBody @Valid UserDTO dto) {
// 校验失败直接抛 MethodArgumentNotValidException,被全局 handler 接住
return Result.ok(userService.create(dto));
}Query 参数(GET):
java
@GetMapping("/search")
public Result<List<UserVO>> search(@Valid UserQuery query) {
return Result.ok(userService.search(query));
}
@Data
public class UserQuery {
@NotBlank(message = "关键词不能为空")
private String keyword;
@Min(value = 1, message = "页码最小 1")
private int page = 1;
}路径参数:
java
@GetMapping("/{id}")
public Result<UserVO> getById(@PathVariable @Min(1) Long id) {
return Result.ok(userService.getById(id));
}⚠️ 坑 1:
@RequestBody用@Valid,@RequestParam/@PathVariable用@Validated(在 controller 类上)。
为什么分两个?触发校验的机制不同:
| 注解 | 来源 | 用在哪 | 触发时机 |
|---|---|---|---|
@Valid | JSR-303 标准 | @RequestBody 旁边 | Spring 反序列化对象时顺手校验 |
@Validated | Spring 自带 | controller 类上 | 给方法参数(@RequestParam/@PathVariable)开方法拦截 |
关键两条规则:
@PathVariable/@RequestParam上的约束(比如@Min(1)),类上必须加@Validated,否则不生效——运行时不会报错,等于没校验。@Valid和@Validated不冲突,一个类可以两个都出现:类上加@Validated(给路径参数),@RequestBody旁边加@Valid(给对象),各管各的。
java
@RestController
@RequestMapping("/users")
@Validated // ← 路径参数校验需要
public class UserController {
@GetMapping("/{id}")
public Result<UserVO> getById(@PathVariable @Min(1) Long id) { }
}四、统一处理校验异常
java
@ExceptionHandler(MethodArgumentNotValidException.class) // @RequestBody
public Result<Void> handleBody(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.error(400, msg);
}
@ExceptionHandler(ConstraintViolationException.class) // @RequestParam / @PathVariable
public Result<Void> handleParam(ConstraintViolationException e) {
String msg = e.getConstraintViolations().stream()
.map(v -> v.getMessage())
.collect(Collectors.joining("; "));
return Result.error(400, msg);
}五、分组校验
💡 为什么这里用
@Validated(Groups.Xxx.class)而不是@Valid?
@Validated在 Spring 里其实有三种用法,和@Valid不冲突,只是分工不同:
用法 位置 用途 类上 @Validatedcontroller 类 给 @RequestParam/@PathVariable开方法拦截(第 48 章坑 1)参数上 @Validated(Groups.Xxx.class)@RequestBody旁边分组校验(本节) 参数上 @Valid@RequestBody旁边普通校验,不支持分组 简单校验 →
@Valid够用;分组校验 → 必须@Validated(Groups.Xxx.class),因为 JSR-303 的@Valid不支持 groups 写法(@Valid(Groups.Create.class)编译报错)。
场景:同一个 DTO,创建时 ID 为空,更新时 ID 必填。
java
public interface Groups {
interface Create {}
interface Update {}
}
@Data
public class UserDTO {
@Null(groups = Groups.Create.class, message = "创建时 ID 必须为空")
@NotNull(groups = Groups.Update.class, message = "更新时 ID 不能为空")
private Long id;
@NotBlank(groups = {Groups.Create.class, Groups.Update.class})
private String username;
}
// Controller
@PostMapping
public Result<Void> create(@RequestBody @Validated(Groups.Create.class) UserDTO dto) { }
@PutMapping
public Result<Void> update(@RequestBody @Validated(Groups.Update.class) UserDTO dto) { }六、自定义校验
场景:状态字段只能是 0/1/2。
java
@Target({ElementType.FIELD}) // 只能贴在字段上
@Retention(RetentionPolicy.RUNTIME) // 运行时反射要读到
@Constraint(validatedBy = StatusValidator.class) // 把注解和校验器绑一起
public @interface ValidStatus {
String message() default "状态值非法"; // 失败时的提示文案
Class<?>[] groups() default {}; // 分组(配合分组校验)
Class<? extends Payload>[] payload() default {}; // 固定写法,基本不用
}
public class StatusValidator implements ConstraintValidator<ValidStatus, Integer> {
private static final Set<Integer> ALLOWED = Set.of(0, 1, 2);
@Override
public boolean isValid(Integer value, ConstraintValidatorContext ctx) {
return value == null || ALLOWED.contains(value); // null 放行,只允许 0/1/2
}
}用法:
java
public class UserDTO {
@ValidStatus // 字段上贴注解,框架自动跑 isValid
private Integer status;
}关键两点:
@Constraint(validatedBy = ...)是核心:把注解和校验器绑起来。改逻辑改isValid就好,不动注解。value == null默认放行——null 由@NotNull管,避免和自定义校验冲突。
使用:
java
@ValidStatus
private Integer status;七、本章小结
| 要点 | 关键 |
|---|---|
| Bean 校验 | @Valid + 注解 |
| 路径校验 | @Validated 在 controller |
| 异常 | MethodArgumentNotValidException |
| 分组 | @Validated(Groups.Create.class) |
| 自定义 | @Constraint + ConstraintValidator |
动手练习
- 给
UserDTO加校验:用户名 3-20、邮箱格式、年龄 0-150 - 实现修改密码接口,旧密码 / 新密码各自校验
下一章:第 49 章:MyBatis Plus 入门 →