Skip to content

Spring 参数校验与数据校验

企业应用里,很多 Bug 不是出在“业务逻辑不会写”,而是出在脏数据太早进入了系统,所以校验这条线非常关键。

Spring 里的参数校验和数据校验主要解决的是:

  1. 请求参数是否合法
  2. 对象字段是否满足约束
  3. 规则错误应该在进入业务前尽早拦住
  4. 校验异常如何统一返回

这篇文章重点讲:

  1. Spring 校验在解决什么问题
  2. Bean Validation 是什么
  3. @Valid@Validated 有什么区别
  4. 常见校验注解怎么用
  5. 统一异常处理如何配合校验

1. 为什么校验是企业开发刚需

如果没有统一校验,应用很容易出现这些问题:

  1. 空值、脏值一路流进业务层
  2. 非法状态直到落库才报错
  3. 每个接口自己写 if 判断,风格混乱
  4. 错误信息不统一,前后端协作体验差

所以校验的核心目标不是“多写几条规则”,而是把错误尽量前置,让非法数据尽早被拦在系统边界之外。


2. Spring 校验主要建立在什么基础上

在 Spring 生态里,这条线最常见的基础是 Bean Validation 规范。

你可以把它理解成一套给 Java 对象声明约束规则的标准化模型。

例如:

  1. 不能为空
  2. 长度范围
  3. 数值范围
  4. 邮箱格式
  5. 自定义校验规则

Spring Boot 则负责把这套能力更自然地接进 Web 参数绑定和对象校验流程。


3. @Valid@Validated 有什么区别

这是校验主线里最常见的问题。

3.1 @Valid

它来自 Jakarta Validation,更适合先理解成“触发对象校验”。

3.2 @Validated

它是 Spring 提供的增强版校验入口。除了能触发校验外,它还更常用于支持分组校验。

可以直接这样区分:

  1. @Valid 更偏标准校验入口
  2. @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());
    }
}

这里真正发生的事情是:

  1. 请求体先绑定成 Java 对象
  2. 再执行字段约束校验
  3. 校验不通过时,直接抛异常
  4. 业务方法不会继续执行

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. 分组校验到底在解决什么问题

有些场景下,同一个对象在不同操作里约束不一样。

例如:

  1. 新增时 id 不需要传
  2. 更新时 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. 自定义校验注解什么时候需要

内置注解能解决很多问题,但企业里经常还会遇到:

  1. 手机号格式
  2. 枚举值合法性
  3. 身份证号规则
  4. 某种业务编码规则

这时就需要自定义注解和校验器。

例如一个简化示意:

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. 校验异常为什么一定要统一处理

如果不统一处理,项目里很容易变成:

  1. 这个接口返回字符串
  2. 那个接口返回默认 HTML 错页
  3. 另一个接口返回一堆框架默认异常

这对前后端协作非常不友好。

所以企业项目通常会配统一异常处理。

例如:

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触发标准对象校验
@ValidatedSpring 增强版校验入口,支持分组
@RestControllerAdvice统一异常处理
@ExceptionHandler处理指定异常
ConstraintValidator自定义校验器接口

12. 工程上最容易踩的几个坑

12.1 误区一:数据库有约束,就不需要应用层校验

数据库约束很重要,但它更多是最后防线。

应用层校验的价值在于:更早失败、更友好返回、更贴近业务规则。

12.2 误区二:@Valid@Validated 完全一样

它们重叠很多,但在分组校验、方法参数校验场景下,@Validated 更常见。

12.3 误区三:只要字段上写注解,校验就一定自动生效

不是。

要看入口是否真正触发了校验流程。

12.4 误区四:校验只属于 Controller

Controller 是最常见入口,但服务内部、消息消费、定时任务输入对象也可能需要校验。


13. 一句话总结

Spring 校验这条线,本质上是在解决非法数据如何尽早被拦在系统边界之外;而企业里真正要把它用好,关键在于统一规则表达、统一异常处理,以及分清对象校验、方法参数校验和分组校验这几个边界。

基于 VitePress 构建的个人技术笔记。