参数校验
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 | 邮箱格式 |
@URL | URL 格式(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);
}
}