Spring Boot: 数据验证
最后更新:2026-08-26
数据验证是 API 的第一道防线——无效输入不应进入业务逻辑,越早拒绝越好。
1. 你将学到
@Valid/@Validated注解与验证触发机制- 常用约束注解:
@NotNull/@Size/@Pattern/@Email/@Min/@Max - 自定义验证器
ConstraintValidator<A, T>实现 - 分组验证
groups按场景区分校验规则(Create vs Update) - 验证错误响应的统一格式化处理
2. 一个 API 开发者的真实故事
(1) 痛点:脏数据入库
Alice 发现 OrderFlow 数据库中出现了奇怪的数据:订单数量为 -5、邮箱格式为 "abc"、商品价格为 0。Bob 报告说有用户通过 API 提交了负数库存的商品,导致报表统计异常。Alice 之前在 Service 层写了一堆 if-else 校验代码,既冗长又容易遗漏。
(2) Bean Validation 的解法
用注解声明校验规则,Spring 自动触发验证:
JAVA
public record CreateOrderRequest(
@NotNull Long productId,
@Min(1) @Max(100) Integer quantity,
@Email String customerEmail
) {}
无效请求在进入 Controller 之前就被拒绝了。
(3) 收益
Alice 用 Bean Validation 替换了所有手动校验代码,Controller 代码量减少 40%,再也不会遗漏校验规则。数据库中不再出现脏数据。
3. Bean Validation 注解体系
(1) 验证执行流程
graph TD
A["Client Request<br/>@RequestBody"] --> B{"@Valid<br/>Triggered?"}
B -->|Yes| C["Hibernate Validator<br/>Check Constraints"]
C --> D{"All Valid?"}
D -->|Yes| E["Controller Method<br/>Executes"]
D -->|No| F["MethodArgumentNotValidException<br/>400 Bad Request"]
B -->|No| G["Skip Validation<br/>Potential dirty data"]
(3) 验证触发方式
| 注解 | 用途 | 放置位置 |
|---|---|---|
@Valid |
触发级联验证(包括嵌套对象) | 方法参数、字段 |
@Validated |
支持分组验证 | 类、方法参数 |
@Validated(Group.class) |
指定验证分组 | 方法参数 |
▶ 示例: Controller 参数验证
JAVA
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@PostMapping
public ResponseEntity<Order> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
// If validation fails, MethodArgumentNotValidException is thrown
// before reaching this line
Order order = orderService.createOrder(request);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
}
public record CreateOrderRequest(
@NotNull(message = "Product ID is required")
Long productId,
@Min(value = 1, message = "Quantity must be at least 1")
@Max(value = 100, message = "Quantity cannot exceed 100")
Integer quantity,
@Email(message = "Invalid email format")
String customerEmail
) {}
输出:
TEXT
📖 仅展示
// 执行成功
4. 常用约束注解
(1) 约束注解速查
| 注解 | 适用类型 | 说明 | 示例 |
|---|---|---|---|
@NotNull |
所有类型 | 不能为 null | @NotNull Long id |
@NotBlank |
String | 不能为空/null/纯空格 | @NotBlank String name |
@NotEmpty |
String/Collection | 不能为空/null | @NotEmpty List<String> tags |
@Size |
String/Collection | 长度/大小范围 | @Size(min=2, max=100) |
@Min / @Max |
数字类型 | 值范围 | @Min(0) @Max(99999) |
@Positive |
数字类型 | 正数 | @Positive BigDecimal price |
@Email |
String | 邮箱格式 | @Email String email |
@Pattern |
String | 正则匹配 | @Pattern(regexp="^[A-Z]") |
@Past / @Future |
日期类型 | 过去/未来 | @Past LocalDate birthDate |
▶ 示例: Product DTO 验证
JAVA
public record CreateProductRequest(
@NotBlank(message = "Product name is required")
@Size(min = 2, max = 200, message = "Name must be 2-200 characters")
String name,
@NotNull(message = "Price is required")
@Positive(message = "Price must be positive")
@DecimalMin(value = "0.01", message = "Price must be at least 0.01")
BigDecimal price,
@NotNull(message = "Stock is required")
@Min(value = 0, message = "Stock cannot be negative")
Integer stock,
@Email(message = "Supplier email must be valid")
String supplierEmail,
@Pattern(regexp = "^[A-Z]{3}-\\d{4}$", message = "SKU format: XXX-0000")
String sku
) {}
输出:
TEXT
📖 仅展示
// 执行成功
| 注解对比 | null | "" | " " | "abc" |
|---|---|---|---|---|
@NotNull |
❌ | ✅ | ✅ | ✅ |
@NotBlank |
❌ | ❌ | ❌ | ✅ |
@NotEmpty |
❌ | ❌ | ✅ | ✅ |
🔥 易错: String 类型验证优先用
@NotBlank 而非 @NotNull,因为空字符串通常也是无效的。
5. 自定义验证器
(1) 实现步骤
- 定义约束注解
- 实现
ConstraintValidator<A, T> - 在 DTO 字段上使用
▶ 示例: 自定义 @ValidOrderQuantity 验证器
JAVA
// Step 1: Define constraint annotation
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = OrderQuantityValidator.class)
public @interface ValidOrderQuantity {
String message() default "Order quantity exceeds product stock limit";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// Step 2: Implement ConstraintValidator
public class OrderQuantityValidator
implements ConstraintValidator<ValidOrderQuantity, Integer> {
private static final int MAX_QUANTITY_PER_ITEM = 100;
@Override
public boolean isValid(Integer quantity, ConstraintValidatorContext context) {
if (quantity == null) {
return true; // Let @NotNull handle null check
}
return quantity >= 1 && quantity <= MAX_QUANTITY_PER_ITEM;
}
}
// Step 3: Use in DTO
public record CreateOrderRequest(
@NotNull Long productId,
@ValidOrderQuantity Integer quantity
) {}
输出:
TEXT
📖 仅展示
// 执行成功
6. 分组验证
(1) 按场景区分校验规则
Create 和 Update 场景通常需要不同的验证规则:
| 场景 | Product ID | Name | Price |
|---|---|---|---|
| Create | 自动生成,不需要 | 必填 | 必填 |
| Update | 必填(标识修改谁) | 可选 | 可选 |
▶ 示例: 分组验证
JAVA
// Define group interfaces
public interface Create {}
public interface Update {}
// DTO with group-aware validation
public record ProductRequest(
@Null(groups = Create.class, message = "ID must be null for creation")
@NotNull(groups = Update.class, message = "ID is required for update")
Long id,
@NotBlank(groups = Create.class, message = "Name is required for creation")
@Size(min = 2, max = 200)
String name,
@NotNull(groups = Create.class, message = "Price is required for creation")
@Positive BigDecimal price
) {}
输出:
TEXT
📖 仅展示
// 执行成功
JAVA
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
@PostMapping
public ResponseEntity<Product> create(
@Validated(Create.class) @RequestBody ProductRequest request) {
// Only Create group validations are applied
// ...
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@PutMapping("/{id}")
public ResponseEntity<Product> update(
@PathVariable Long id,
@Validated(Update.class) @RequestBody ProductRequest request) {
// Only Update group validations are applied
// ...
return ResponseEntity.ok().build();
}
}
▶ 示例: 嵌套验证
JAVA
public record CreateOrderRequest(
@NotNull Long productId,
@ValidOrderQuantity Integer quantity,
@Valid @NotNull ShippingAddress shippingAddress
) {}
public record ShippingAddress(
@NotBlank String street,
@NotBlank String city,
@NotBlank String zipCode,
@Pattern(regexp = "^[A-Z]{2}$") String country
) {}
输出:
TEXT
📖 仅展示
// 执行成功
📌 重点: 嵌套对象必须加
@Valid,否则嵌套字段验证不生效。@Validated 不支持嵌套级联验证。
7. 验证错误响应格式化
(1) 统一错误响应格式
JAVA
@RestControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidation(
MethodArgumentNotValidException ex) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", Instant.now());
body.put("status", HttpStatus.BAD_REQUEST.value());
List<Map<String, String>> errors = ex.getBindingResult()
.getFieldErrors().stream()
.map(fe -> Map.of(
"field", fe.getField(),
"message", fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "",
"rejectedValue", fe.getRejectedValue() != null ? fe.getRejectedValue().toString() : "null"
))
.toList();
body.put("errors", errors);
return ResponseEntity.badRequest().body(body);
}
}
💻 输出:
JSON
{
"timestamp": "2024-01-15T10:00:00Z",
"status": 400,
"errors": [
{"field": "quantity", "message": "Quantity must be at least 1", "rejectedValue": "0"},
{"field": "customerEmail", "message": "Invalid email format", "rejectedValue": "abc"}
]
}
8. 综合示例:OrderFlow 完整验证体系
JAVA
// Validation groups
package com.orderflow.validation;
public interface Create {}
public interface Update {}
// Custom validator: @ValidShippingAddress
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ShippingAddressValidator.class)
public @interface ValidShippingAddress {
String message() default "Invalid shipping address";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class ShippingAddressValidator
implements ConstraintValidator<ValidShippingAddress, String> {
@Override
public boolean isValid(String address, ConstraintValidatorContext ctx) {
if (address == null || address.isBlank()) return false;
return address.length() >= 10 && address.length() <= 500;
}
}
// DTOs
public record CreateOrderRequest(
@NotNull(groups = Create.class) Long productId,
@Min(value = 1, message = "Quantity must be at least 1")
@Max(value = 100, message = "Quantity cannot exceed 100")
Integer quantity,
@Email String customerEmail,
@ValidShippingAddress String shippingAddress
) {}
public record CreateProductRequest(
@Null(groups = Create.class) @NotNull(groups = Update.class) Long id,
@NotBlank(groups = Create.class) @Size(min = 2, max = 200) String name,
@NotNull(groups = Create.class) @Positive BigDecimal price,
@Min(0) Integer stock,
@Pattern(regexp = "^[A-Z]{3}-\\d{4}$", message = "SKU: XXX-0000") String sku
) {}
// Controller
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@PostMapping
public ResponseEntity<Void> create(
@Validated(Create.class) @RequestBody CreateOrderRequest req) {
// Validation passed, proceed to business logic
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
❓ 常见问题
Q @Valid 和 @Validated 有什么区别?
A @Valid 是 JSR-380 标准注解,支持嵌套级联验证。@Validated 是 Spring 扩展注解,支持分组验证。需要分组用 @Validated,需要嵌套用 @Valid,两者可以组合使用。
Q 验证失败返回什么?
A Spring Boot 默认返回 400 Bad Request + JSON 错误信息。可通过 @RestControllerAdvice 自定义格式,本课提供了统一格式方案。
Q @NotBlank、@NotEmpty、@NotNull 怎么选?
A String 用 @NotBlank(不允许 null、空串、纯空格),Collection 用 @NotEmpty(不允许 null、空集合),其他类型用 @NotNull。
Q 分组验证和默认验证可以同时生效吗?
A 默认情况下,指定分组后 Default 组不生效。如果想同时生效,让自定义组继承 Default:
public interface Create extends Default {}。Q 自定义验证器中能注入 Spring Bean 吗?
A 可以。ConstraintValidator 由 Spring 容器管理,可以在 isValid 方法中使用 @Autowired 注入 Bean(如查询数据库验证唯一性)。
Q 如何国际化验证错误消息?
A 在
resources/ValidationMessages.properties 中定义消息 key,如 order.quantity.invalid=Order quantity must be between {min} and {max},支持多语言文件。📖 小节
@Valid/@Validated触发验证,验证失败抛MethodArgumentNotValidException- 常用注解:String 用
@NotBlank,数字用@Min/@Max/@Positive,邮箱用@Email - 自定义验证器三步:定义注解 → 实现 ConstraintValidator → 使用
- 分组验证区分 Create/Update 场景,
@Validated(Group.class)指定分组 - 嵌套对象必须加
@Valid才能级联验证 @RestControllerAdvice统一格式化验证错误响应
📝 作业
-
基础题(难度⭐):为 OrderFlow 的 CreateOrderRequest 和 CreateProductRequest 添加 Bean Validation 注解,验证无效输入返回 400 错误。
-
进阶题(难度⭐⭐):实现分组验证——Create 时 name 和 price 必填,Update 时 id 必填且 name/price 可选。实现自定义
@ValidShippingAddress验证器。 -
挑战题(难度⭐⭐⭐):创建一个
@UniqueProductSku验证器,注入 ProductRepository 检查 SKU 是否已存在,实现数据库级别的唯一性校验,思考验证器与业务层的职责边界。