Skip to content
第 48 章 后端 ⏱ 8 分钟阅读

第 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 类上)。

为什么分两个?触发校验的机制不同:

注解来源用在哪触发时机
@ValidJSR-303 标准@RequestBody 旁边Spring 反序列化对象时顺手校验
@ValidatedSpring 自带controller 类上给方法参数(@RequestParam/@PathVariable)开方法拦截

关键两条规则:

  1. @PathVariable / @RequestParam 上的约束(比如 @Min(1)),类上必须加 @Validated,否则不生效——运行时不会报错,等于没校验。
  2. @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

动手练习 ​

  1. 给 UserDTO 加校验:用户名 3-20、邮箱格式、年龄 0-150
  2. 实现修改密码接口,旧密码 / 新密码各自校验

下一章:第 49 章:MyBatis Plus 入门 →

本站基于 VitePress 构建 · 由 StackHub 团队维护