1.快速集成
A 添加依赖
在 Spring Boot 3.x (2026年主流版本) 中,spring-boot-starter-validation 是进行参数校验的标准且唯一推荐 的方式。它基于 Jakarta Bean Validation 3.0 (原 JSR-380) 规范,底层默认使用 Hibernate Validator 实现。
在 Spring Boot 2.3 之前,该依赖包含在 spring-boot-starter-web 中。但从 2.3 开始(包括现在的 3.x/4.x),必须显式添加。
Maven (pom.xml):
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2. 核心用法:三步走
第一步:在 DTO/VO 类上定义规则
使用注解标记字段的约束条件。
java
package com.example.demo.dto;
import jakarta.validation.constraints.*;
import lombok.Data;
@Data
public class UserRegisterDTO {
@NotNull(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;
@NotBlank(message = "密码不能为空") // 专门用于字符串,检查 null, "", " "
@Pattern(regexp = "^[a-zA-Z0-9]{6,20}$", message = "密码必须是6-20位字母或数字")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 18, message = "必须年满18岁")
@Max(value = 100, message = "年龄不能超过100岁")
private Integer age;
@Past(message = "出生日期必须是过去的时间")
private java.time.LocalDate birthday;
// 嵌套对象校验 (见下文高级篇)
@Valid
private AddressDTO address;
}
第二步:在 Controller 中启用校验
在参数前添加 @Valid (或 Spring 特有的 @Validated)。
java
import jakarta.validation.Valid;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
/**
* 方式一:配合 BindingResult 手动处理错误
* 如果校验失败,errors.hasErrors() 为 true
*/
@PostMapping("/register-v1")
public ResponseEntity<?> registerV1(@Valid @RequestBody UserRegisterDTO dto,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// 手动提取错误信息
StringBuilder errorMsg = new StringBuilder();
bindingResult.getFieldErrors().forEach(err ->
errorMsg.append(err.getField()).append(":").append(err.getDefaultMessage()).append("; ")
);
return ResponseEntity.badRequest().body(errorMsg.toString());
}
// 业务逻辑...
return ResponseEntity.ok("注册成功");
}
/**
* 方式二:推荐!配合全局异常处理器 (见第三步)
* 不需要 BindingResult,校验失败直接抛异常
*/
@PostMapping("/register-v2")
public ResponseEntity<?> registerV2(@Valid @RequestBody UserRegisterDTO dto) {
// 如果代码能执行到这里,说明校验已通过
return ResponseEntity.ok("注册成功 (v2)");
}
}
第三步:全局统一异常处理 (最佳实践)
不要让每个 Controller 都写 if (bindingResult.hasErrors())。创建一个全局Advice来捕获 MethodArgumentNotValidException。
java
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalValidationExceptionHandler {
/**
* 捕获 @Valid 校验失败的异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
// 遍历所有字段错误
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
// 返回格式:{ "username": "用户名不能为空", "password": "密码必须是..." }
return ResponseEntity.badRequest().body(errors);
}
/**
* 捕获 @RequestParam / @PathVariable 校验失败的异常 (需配合 @Validated)
*/
@ExceptionHandler(org.springframework.validation.ConstraintViolationException.class)
public ResponseEntity<Map<String, String>> handleConstraintViolationException(
org.springframework.validation.ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(violation -> {
String path = violation.getPropertyPath().toString();
errors.put(path, violation.getMessage());
});
return ResponseEntity.badRequest().body(errors);
}
}
3. 常用注解速查表 (Jakarta 版)
| 注解 | 适用类型 | 说明 |
|---|---|---|
@NotNull |
任何对象 | 不能为 null,但可以是空字符串 ""。 |
@NotBlank |
String |
不能为 null,不能为空字符串 "",也不能全是空格 " "。字符串首选。 |
@NotEmpty |
集合/数组/String | 不能为 null,且大小/长度必须 > 0。 |
@Size |
String/Collection/Array | 限制长度或大小 (min, max)。 |
@Min / @Max |
数值类型 | 限制数值范围。 |
@DecimalMin |
BigDecimal | 限制小数范围。 |
@Email |
String |
校验邮箱格式。 |
@Pattern |
String |
正则表达式校验。 |
@Past / @Future |
Date/LocalDate | 时间必须是过去或未来。 |
@Positive |
数值类型 | 必须是正数 (>0)。 |
@Valid |
对象/集合 | 级联校验。如果字段是对象或 List<Object),加上此注解会递归校验内部字段。 |
4. 高级功能
A. 分组校验 (Group Validation)
场景:同一个 DTO,新增 时 ID 应为 null,修改时 ID 必填。
- 定义分组接口:
java
public interface CreateGroup {}
public interface UpdateGroup {}
- 在 DTO 中指定分组:
java
public class UserDTO {
// 仅在更新时需要校验
@NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
private Long id;
// 仅在创建时需要校验
@NotBlank(groups = CreateGroup.class, message = "创建时用户名必填")
private String username;
}
- 在 Controller 中指定分组:
java
// 创建接口:只校验 CreateGroup
@PostMapping
public String create(@Validated(CreateGroup.class) @RequestBody UserDTO dto) { ... }
// 更新接口:只校验 UpdateGroup
@PutMapping
public String update(@Validated(UpdateGroup.class) @RequestBody UserDTO dto) { ... }
B. 自定义校验注解
场景:校验手机号、身份证、或特定的业务规则(如"结束时间必须大于开始时间")。
- 创建注解:
java
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class) // 指定验证器
public @interface IsValidPhone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- 实现验证器逻辑:
java
public class PhoneValidator implements ConstraintValidator<IsValidPhone, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true; // null 由 @NotNull 处理
return value.matches("^1[3-9]\d{9}$"); // 简单正则
}
}
- 使用:
java
@IsValidPhone(message = "请输入正确的中国大陆手机号")
private String phone;
C. 跨字段校验 (类级别注解)
场景:endTime 必须大于 startTime。
java
@Data
@AssertTrue(message = "结束时间必须晚于开始时间", property = "endTime") // 这种写法较难定位具体字段,通常用类级别约束
public class EventDTO {
private LocalDateTime startTime;
private LocalDateTime endTime;
// 更好的方式:自定义类级别注解
}
// 或者直接在类上加逻辑判断,或在自定义注解中获取整个对象
更优雅的做法是自定义一个类级别注解 @Constraint(validatedBy = EventValidator.class),在 Validator 中拿到整个 EventDTO 对象进行比较。
5. 常见坑与注意事项
-
@Validvs@Validated:@Valid(Jakarta 标准): 支持嵌套校验,不支持分组。@Validated(Spring 扩展): 支持分组校验,支持在类级别开启校验。- 建议 :Controller 参数优先用
@Valid(兼容性好);如果需要分组,必须用@Validated。
-
嵌套对象失效:
- 如果 DTO 中包含另一个对象(如
private Address address;),必须在字段前加@Valid,否则内部字段不会校验。 - 如果是
List<Address>,同样需要@Valid。
- 如果 DTO 中包含另一个对象(如
-
基本数据类型 vs 包装类型:
int age:默认值是 0,@Min(1)可能会失效(因为 0 也是值),或者无法区分"未传参"和"传了0"。- 建议 :校验参数尽量使用包装类型 (
Integer,Long),配合@NotNull使用。
-
性能问题:
- 正则表达式 (
@Pattern) 复杂时会消耗 CPU,尽量避免在高频接口使用极其复杂的正则。
- 正则表达式 (
6.这是一份针对 Spring Boot 3.x / Jakarta Bean Validation 3.0 的注解速查表。
⚠️ 重要提示:
- Spring Boot 3+ 必须使用
jakarta.validation.constraints.*包。- 旧版 Spring Boot 2.x 使用
javax.validation.constraints.*。- 不要混用,否则校验会直接失效且不报错!
1. 空值与字符串检查 (最常用)
表格
| 注解 | 适用类型 | 规则说明 | 典型场景 |
|---|---|---|---|
@NotNull |
任何对象 | 不能为 null。 ✅ 允许 "", " ", 0 |
非字符串对象、包装类型 (Integer, Date) |
@NotBlank |
String |
不能为 null,且去除空格后长度 > 0。 ❌ 拒绝 null, "", " " |
用户名、密码、标题 (字符串首选) |
@NotEmpty |
String, Collection, Map, Array |
不能为 null,且长度/大小 > 0。 ❌ 拒绝 null, "", [] |
列表参数、非空字符串 |
@Null |
任何对象 | 必须为 null |
某些特定状态字段 |
2. 数值大小检查
表格
| 注解 | 适用类型 | 规则说明 | 属性示例 |
|---|---|---|---|
@Min(value) |
数值类型 (int, long, BigDecimal等) |
值必须 ≥ value | @Min(18) (年龄最小18) |
@Max(value) |
数值类型 | 值必须 ≤ value | @Max(100) (分数最大100) |
@DecimalMin(value) |
BigDecimal, String |
值必须 ≥ value (支持小数) | @DecimalMin("0.01") |
@DecimalMax(value) |
BigDecimal, String |
值必须 ≤ value (支持小数) | @DecimalMax("99.99") |
@Positive |
数值类型 | 必须 > 0 (正数) | 价格、数量 |
@PositiveOrZero |
数值类型 | 必须 ≥ 0 (非负数) | 余额、积分 |
@Negative |
数值类型 | 必须 < 0 (负数) | 亏损额 |
@NegativeOrZero |
数值类型 | 必须 ≤ 0 (非正数) | 抵扣额 |
3. 格式与模式检查
表格
| 注解 | 适用类型 | 规则说明 | 属性示例 |
|---|---|---|---|
@Email |
String |
必须是合法的邮箱格式 | @Email(message="邮箱不对") |
@Pattern(regexp) |
String |
必须符合正则表达式 | @Pattern(regexp="^1[3-9]\d{9}$") (手机号) |
@Size(min, max) |
String, Collection, Array |
长度/大小必须在范围内 | @Size(min=6, max=20) (密码长度) |
@Digits(integer, fraction) |
数值类型 | 整数位和小数位限制 | @Digits(integer=5, fraction=2) (金额) |
4. 日期与时间检查
表格
| 注解 | 适用类型 | 规则说明 |
|---|---|---|
@Past |
Date, LocalDate, LocalDateTime |
必须是过去的时间 (昨天及以前) |
@PastOrPresent |
日期类型 | 必须是过去或现在 (不能是未来) |
@Future |
日期类型 | 必须是未来的时间 (明天及以后) |
@FutureOrPresent |
日期类型 | 必须是未来或现在 (不能是过去) |
5. 布尔与逻辑检查
表格
| 注解 | 适用类型 | 规则说明 |
|---|---|---|
@AssertTrue |
boolean, Boolean |
必须为 true |
@AssertFalse |
boolean, Boolean |
必须为 false |
6. 级联与分组 (高级控制)
表格
| 注解 | 作用域 | 说明 |
|---|---|---|
@Valid |
字段 (对象/集合) | 级联校验 。如果字段是对象或 List<Object>,加上此注解会递归校验内部属性。非常重要,常漏加! |
@Validated |
类/方法参数 | Spring 提供的扩展注解。 1. 支持分组校验 (@Validated(Group.class)) 2. 支持在 Controller 类级别开启校验 |
💡 避坑指南 & 最佳实践
1. @NotNull vs @NotBlank vs @NotEmpty
这是面试和开发中最容易混淆的:
-
对象/数字 :只用
@NotNull。 -
字符串 :优先用
@NotBlank(它涵盖了 null 和 空串/空格)。- ❌ 错误:
@NotNull+@NotEmpty(重复且@NotEmpty不检查空格) - ✅ 正确:
@NotBlank
- ❌ 错误:
-
集合/数组 :用
@NotEmpty(集合没有"空白"概念,只有空)。
2. 基本类型 vs 包装类型
- 基本类型 (
int,double) :默认有初始值 (0, 0.0),@NotNull对它们无效 (因为它们永远不为 null)。 - 建议 :DTO 中的校验字段全部使用 包装类型 (
Integer,Double,Long),这样@NotNull才能生效,区分"未传参"和"传了0"。
3. 嵌套对象必须加 @Valid
如果不加 @Valid,内部的 Address 对象即使填了非法数据,也不会报错。
java
public class UserDTO {
@NotNull
private String name;
// ❌ 错误写法:address 内部的字段不会校验
private Address address;
// ✅ 正确写法:开启级联校验
@Valid
private Address address;
}
4. 自定义错误消息
所有注解都支持 message 属性,支持占位符:
java
@Size(min = 2, max = 10, message = "长度必须在 {min} 到 {max} 之间,当前长度为 {validatedValue}")
5. 常用组合模板
- 用户名 :
@NotBlank @Size(min=2, max=20) - 密码 :
@NotBlank @Pattern(regexp="...") - 年龄 :
@NotNull @Min(1) @Max(150) - 邮箱 :
@NotBlank @Email - 手机号 :
@NotBlank @Pattern(regexp="^1[3-9]\d{9}$") - 状态枚举 :
@NotNull(配合后端逻辑判断) - 开始/结束时间 :
@NotNull+ 自定义类级别注解 (比较两个时间)
总结流程图
掌握这套流程,你的 Spring Boot 接口将具备强大的防御能力,杜绝脏数据进入业务层。