Appearance
Spring 参数校验与数据校验
企业应用里,很多 Bug 不是出在“业务逻辑不会写”,而是出在脏数据太早进入了系统,所以校验这条线非常关键。
Spring 里的参数校验和数据校验主要解决的是:
- 请求参数是否合法
- 对象字段是否满足约束
- 规则错误应该在进入业务前尽早拦住
- 校验异常如何统一返回
这篇文章重点讲:
- Spring 校验在解决什么问题
Bean Validation是什么@Valid和@Validated有什么区别- 常见校验注解怎么用
- 统一异常处理如何配合校验
1. 为什么校验是企业开发刚需
如果没有统一校验,应用很容易出现这些问题:
- 空值、脏值一路流进业务层
- 非法状态直到落库才报错
- 每个接口自己写 if 判断,风格混乱
- 错误信息不统一,前后端协作体验差
所以校验的核心目标不是“多写几条规则”,而是把错误尽量前置,让非法数据尽早被拦在系统边界之外。
2. Spring 校验主要建立在什么基础上
在 Spring 生态里,这条线最常见的基础是 Bean Validation 规范。
你可以把它理解成一套给 Java 对象声明约束规则的标准化模型。
例如:
- 不能为空
- 长度范围
- 数值范围
- 邮箱格式
- 自定义校验规则
Spring Boot 则负责把这套能力更自然地接进 Web 参数绑定和对象校验流程。
3. @Valid 和 @Validated 有什么区别
这是校验主线里最常见的问题。
3.1 @Valid
它来自 Jakarta Validation,更适合先理解成“触发对象校验”。
3.2 @Validated
它是 Spring 提供的增强版校验入口。除了能触发校验外,它还更常用于支持分组校验。
可以直接这样区分:
@Valid更偏标准校验入口@Validated更偏 Spring 场景增强,尤其是分组校验
4. 最常见的校验注解有哪些
这部分很值得整理成表。
| 注解 | 作用 |
|---|---|
@NotNull | 不能为 null |
@NotBlank | 字符串不能为空且去空格后不能空 |
@NotEmpty | 集合、数组、字符串等不能为空 |
@Size | 长度或大小范围 |
@Min / @Max | 数值最小值 / 最大值 |
@Positive | 必须为正数 |
@Email | 邮箱格式校验 |
@Pattern | 正则表达式校验 |
@Past / @Future | 时间是否在过去 / 未来 |
例如:
java
/**
* 创建用户接口的请求对象。
*/
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在 2 到 20 之间")
private String name;
@Min(value = 1, message = "年龄不能小于 1")
@Max(value = 120, message = "年龄不能大于 120")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
// getter/setter
}5. 在 Controller 里最常见的校验方式长什么样
这是最常见的入口。
java
/**
* 用户接口控制器示例。
*/
@RestController
@RequestMapping("/users")
public class UserController {
/**
* 创建用户,并在进入方法前触发请求体校验。
*
* @param request 创建用户请求体
* @return 创建后的用户视图对象
*/
@PostMapping
public UserVO create(@Valid @RequestBody CreateUserRequest request) {
return new UserVO(1L, request.getName());
}
}这里真正发生的事情是:
- 请求体先绑定成 Java 对象
- 再执行字段约束校验
- 校验不通过时,直接抛异常
- 业务方法不会继续执行
6. 路径参数和查询参数怎么校验
很多人以为校验只用于 @RequestBody,其实不是。
对于方法参数本身,也可以做校验。
例如:
java
/**
* 订单接口控制器示例。
*/
@RestController
@RequestMapping("/orders")
@Validated
public class OrderController {
/**
* 按订单 ID 查询订单,并对路径参数做最小值校验。
*
* @param id 订单主键 ID,要求大于等于 1
* @return 查询到的订单视图对象
*/
@GetMapping("/{id}")
public OrderVO getById(@PathVariable @Min(1) Long id) {
return new OrderVO(id, "CREATED");
}
}这里要特别注意:方法参数校验场景里,类上通常需要配合 @Validated。
7. 嵌套对象校验到底怎么做
企业应用里,请求对象经常不是平铺结构。
如果对象里还套了子对象,就要考虑子对象的校验规则能不能继续递归生效。
这时通常要配合 @Valid。
例如:
java
/**
* 收货地址请求对象。
*/
public class AddressRequest {
@NotBlank
private String city;
// getter/setter
}
/**
* 创建订单请求对象,演示嵌套对象校验。
*/
public class CreateOrderRequest {
@NotBlank
private String productCode;
@Valid
@NotNull
private AddressRequest address;
// getter/setter
}如果不加 @Valid,子对象里的约束往往不会自动递归触发。
8. 分组校验到底在解决什么问题
有些场景下,同一个对象在不同操作里约束不一样。
例如:
- 新增时
id不需要传 - 更新时
id必须传
这时分组校验就很有价值。
例如:
java
/**
* 新增场景的校验分组。
*/
public interface CreateGroup {
}
/**
* 更新场景的校验分组。
*/
public interface UpdateGroup {
}
/**
* 用户新增 / 更新共用的命令对象。
*/
public class UserCommand {
@NotNull(groups = UpdateGroup.class)
private Long id;
@NotBlank(groups = {CreateGroup.class, UpdateGroup.class})
private String name;
// getter/setter
}Controller 中:
java
/**
* 创建用户接口,按新增分组执行校验。
*
* @param command 创建命令对象
*/
@PostMapping
public void create(@RequestBody @Validated(CreateGroup.class) UserCommand command) {
}
/**
* 更新用户接口,按更新分组执行校验。
*
* @param command 更新命令对象
*/
@PutMapping
public void update(@RequestBody @Validated(UpdateGroup.class) UserCommand command) {
}9. 自定义校验注解什么时候需要
内置注解能解决很多问题,但企业里经常还会遇到:
- 手机号格式
- 枚举值合法性
- 身份证号规则
- 某种业务编码规则
这时就需要自定义注解和校验器。
例如一个简化示意:
java
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}java
/**
* 手机号格式校验器。
*/
public class MobileValidator implements ConstraintValidator<Mobile, String> {
/**
* 校验手机号是否满足预期格式。
*
* @param value 待校验的手机号字符串
* @param context 当前校验上下文
* @return `true` 表示格式合法;`false` 表示格式不合法
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && value.matches("^1[3-9]\\\\d{9}$");
}
}10. 校验异常为什么一定要统一处理
如果不统一处理,项目里很容易变成:
- 这个接口返回字符串
- 那个接口返回默认 HTML 错页
- 另一个接口返回一堆框架默认异常
这对前后端协作非常不友好。
所以企业项目通常会配统一异常处理。
例如:
java
/**
* 统一处理参数校验异常。
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 提取字段校验失败信息,并返回统一的 400 响应。
*
* @param ex 请求体校验失败异常
* @return 带有错误提示信息的响应体
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationException(
MethodArgumentNotValidException ex) {
String message = ex.getBindingResult()
.getFieldError()
.getDefaultMessage();
return ResponseEntity.badRequest().body(Map.of("message", message));
}
}11. 校验相关常见注解和组件
| 注解 / 组件 | 作用 |
|---|---|
@Valid | 触发标准对象校验 |
@Validated | Spring 增强版校验入口,支持分组 |
@RestControllerAdvice | 统一异常处理 |
@ExceptionHandler | 处理指定异常 |
ConstraintValidator | 自定义校验器接口 |
12. 工程上最容易踩的几个坑
12.1 误区一:数据库有约束,就不需要应用层校验
数据库约束很重要,但它更多是最后防线。
应用层校验的价值在于:更早失败、更友好返回、更贴近业务规则。
12.2 误区二:@Valid 和 @Validated 完全一样
它们重叠很多,但在分组校验、方法参数校验场景下,@Validated 更常见。
12.3 误区三:只要字段上写注解,校验就一定自动生效
不是。
要看入口是否真正触发了校验流程。
12.4 误区四:校验只属于 Controller
Controller 是最常见入口,但服务内部、消息消费、定时任务输入对象也可能需要校验。
13. 一句话总结
Spring 校验这条线,本质上是在解决非法数据如何尽早被拦在系统边界之外;而企业里真正要把它用好,关键在于统一规则表达、统一异常处理,以及分清对象校验、方法参数校验和分组校验这几个边界。