参数校验

返回 Spring Boot 基础

Spring Boot 集成 Jakarta Bean Validation(原 JSR-303/380),通过注解声明约束,结合 @Valid / @Validated 触发校验,无需手写 if 判断。

依赖:spring-boot-starter-validation(已包含 Hibernate Validator)。


常用校验注解

通用

注解说明
@NotNull不能为 null(允许空字符串)
@NotEmpty不能为 null 且长度 > 0(字符串/集合)
@NotBlank不能为 null 且去空格后长度 > 0(仅字符串)
@Size(min, max)字符串长度或集合大小范围
@Min(value) / @Max(value)数值最小/最大值
@DecimalMin / @DecimalMax精确十进制范围
@Positive / @PositiveOrZero正数 / 非负数
@Negative / @NegativeOrZero负数 / 非正数
@Digits(integer, fraction)整数位数和小数位数
@Range(min, max)数值范围(Hibernate 扩展)

格式

注解说明
@Pattern(regexp)正则表达式匹配
@Email邮箱格式
@URLURL 格式(Hibernate 扩展)

日期

注解说明
@Past / @PastOrPresent过去的日期
@Future / @FutureOrPresent未来的日期

在 Controller 中使用

校验请求体(@RequestBody)

// DTO
public class CreateUserRequest {
 
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度为 2-20 个字符")
    private String username;
 
    @NotBlank(message = "密码不能为空")
    @Size(min = 8, message = "密码至少 8 位")
    private String password;
 
    @NotBlank
    @Email(message = "邮箱格式不正确")
    private String email;
 
    @NotNull
    @Min(value = 1, message = "年龄不能小于 1")
    @Max(value = 150, message = "年龄不能大于 150")
    private Integer age;
}
 
// Controller
@PostMapping("/users")
public UserResponse create(@RequestBody @Valid CreateUserRequest req) {
    return userService.create(req);
}

校验路径参数 / 查询参数

类上需加 @Validated(启用方法级校验):

@RestController
@RequestMapping("/users")
@Validated
public class UserController {
 
    @GetMapping("/{id}")
    public UserResponse get(@PathVariable @Positive(message = "ID 必须为正数") Long id) {
        return userService.findById(id);
    }
 
    @GetMapping
    public Page<UserResponse> list(
            @RequestParam @Min(1) int page,
            @RequestParam @Min(1) @Max(100) int size) {
        return userService.findAll(page, size);
    }
}

嵌套对象与集合校验

public class OrderRequest {
 
    @NotNull
    @Valid                          // 级联校验嵌套对象
    private AddressDTO address;
 
    @NotEmpty
    @Valid                          // 级联校验集合元素
    private List<OrderItemDTO> items;
}
 
public class AddressDTO {
    @NotBlank private String city;
    @NotBlank private String street;
}

分组校验

不同场景(创建/更新)使用不同约束:

public interface OnCreate {}
public interface OnUpdate {}
 
public class UserRequest {
    @Null(groups = OnCreate.class)          // 创建时 id 必须为空
    @NotNull(groups = OnUpdate.class)       // 更新时 id 必须有值
    private Long id;
 
    @NotBlank(groups = {OnCreate.class, OnUpdate.class})
    private String username;
}
 
// Controller 使用 @Validated 指定分组
@PostMapping
public void create(@RequestBody @Validated(OnCreate.class) UserRequest req) { ... }
 
@PutMapping
public void update(@RequestBody @Validated(OnUpdate.class) UserRequest req) { ... }

自定义校验注解

// 1. 定义注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@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 ctx) {
        if (value == null) return true;    // null 交给 @NotNull 处理
        return PATTERN.matcher(value).matches();
    }
}
 
// 3. 使用
@Phone
private String phone;

跨字段校验

需用类级别约束(Class-level constraint):

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchValidator.class)
public @interface PasswordMatch {
    String message() default "两次密码不一致";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
 
public class PasswordMatchValidator
        implements ConstraintValidator<PasswordMatch, RegisterRequest> {
 
    @Override
    public boolean isValid(RegisterRequest req, ConstraintValidatorContext ctx) {
        return req.getPassword().equals(req.getConfirmPassword());
    }
}
 
@PasswordMatch
public class RegisterRequest {
    private String password;
    private String confirmPassword;
}

异常处理

校验失败抛出:

异常触发场景
MethodArgumentNotValidException@RequestBody + @Valid 校验失败
ConstraintViolationException方法参数(路径/查询参数)校验失败
BindException@ModelAttribute 表单绑定校验失败

配合 全局异常处理 统一捕获并返回结构化错误:

@RestControllerAdvice
public class GlobalExceptionHandler {
 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleValidation(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .toList();
        return ApiResponse.fail(400, String.join("; ", errors));
    }
 
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleConstraint(ConstraintViolationException ex) {
        String msg = ex.getConstraintViolations()
            .stream()
            .map(ConstraintViolation::getMessage)
            .collect(Collectors.joining("; "));
        return ApiResponse.fail(400, msg);
    }
}

手动触发校验

非 Web 场景(如消费 MQ 消息)手动校验:

@Autowired
private Validator validator;
 
public void process(OrderRequest req) {
    Set<ConstraintViolation<OrderRequest>> violations = validator.validate(req);
    if (!violations.isEmpty()) {
        throw new ConstraintViolationException(violations);
    }
}

相关链接