参数校验是每个后端服务都绕不开的环节。早期写接口时,很多人习惯在方法体里写一堆 if 判断,字段非空、长度限制、格式匹配,层层嵌套下来代码又长又难维护。JSR 380(Bean Validation 2.0)规范提供了一套声明式的校验方案,只需要在实体类字段上打注解,校验逻辑就能自动执行。Spring Boot 对这套规范做了良好封装,本文完整演示如何整合使用。

一、引入依赖与基础配置
从 Spring Boot 2.3 版本开始,spring-boot-starter-web 不再默认携带校验相关的依赖,需要手动引入。引入方式很简单,在 pom.xml 中添加以下依赖即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>这个 starter 底层使用 Hibernate Validator 作为具体实现。引入后不需要任何额外配置,Spring Boot 会自动完成装配。如果你使用的是 Spring Boot 2.3 之前的版本,web starter 已经包含了 validation,无需重复引入。
校验失败时默认会抛出 MethodArgumentNotValidException(用于 RequestBody 参数)或 ConstraintViolationException(用于方法级参数校验),如果不做处理,前端拿到的就是一堆不友好的报错堆栈。所以通常还需要配置全局异常处理,这部分在后面会讲到。
二、常用校验注解与基本用法
先定义一个用户注册的实体类,把常用注解都展示一遍:
public class UserDTO {
@NotNull(message = "用户名不能为空")
private String username;
@NotBlank(message = "昵称不能为空白")
@Size(min = 2, max = 20, message = "昵称长度必须在2到20之间")
private String nickname;
@NotNull(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotNull(message = "年龄不能为空")
@Min(value = 1, message = "年龄最小为1")
@Max(value = 120, message = "年龄最大为120")
private Integer age;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
// 省略 getter 和 setter
}这里要特别注意几个容易混淆的注解区别:@NotNull 只检查对象是否为 null,空字符串能通过校验;@NotEmpty 要求不为 null 且长度大于 0,用在字符串、集合、数组上;@NotBlank 只能用于字符串,会去掉首尾空格后判断是否为空。比如一个字段填了三个空格,@NotNull 和 @NotEmpty 都能通过,只有 @NotBlank 会拦截。
在 Controller 中使用时,给参数对象加上 @Validated 或 @Valid 注解即可触发校验:
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/register")
public String register(@RequestBody @Validated UserDTO userDTO) {
return "注册成功";
}
}当请求参数不满足约束条件时,请求会在进入方法体之前被拦截,根本不会执行业务逻辑,这样就把脏数据挡在了最外层。除了上面这些,常用注解还包括 @AssertTrue、@DecimalMin、@Past(必须是过去时间)、@Future(必须是未来时间)、@Size 等,基本覆盖了日常场景。
三、@Valid 与 @Validated 的区别及分组校验
这两个注解经常被拿来比较。@Valid 是 JSR 标准注解,定义在 jakarta.validation 包中;@Validated 是 Spring 提供的扩展注解。功能上的核心差异有两点:第一,@Validated 支持分组校验,@Valid 不支持;第二,@Validated 可以直接标注在类上开启方法级参数校验,@Valid 只能标注在参数或字段上做级联校验。
分组校验解决的是同一个实体类在不同场景下校验规则不同的问题。比如新增用户时不需要传 id,修改时必须传 id。先定义分组接口:
public interface AddGroup {}
public interface UpdateGroup {}
public class UserDTO {
@Null(groups = {AddGroup.class}, message = "新增时不能指定id")
@NotNull(groups = {UpdateGroup.class}, message = "修改时必须指定id")
private Long id;
@NotNull(groups = {AddGroup.class, UpdateGroup.class}, message = "用户名不能为空")
private String username;
// 省略其他字段
}Controller 中指定分组,只有属于该分组的约束才会生效,没有指定分组的默认约束在分组模式下不会被校验,这一点很多人踩过坑:
@PostMapping("/add")
public String add(@RequestBody @Validated(AddGroup.class) UserDTO userDTO) {
return "新增成功";
}
@PostMapping("/update")
public String update(@RequestBody @Validated(UpdateGroup.class) UserDTO userDTO) {
return "修改成功";
}级联校验也很实用。当实体类中嵌套了另一个对象时,在内层字段上标注 @Valid,校验会递归地进行到嵌套对象内部。注意这里只能用 @Valid,@Validated 没有级联能力,这也是两者配合使用的典型场景。
四、统一异常处理与自定义校验注解
校验失败后给前端返回统一格式的提示信息,是生产环境的基本要求。通过 @RestControllerAdvice 全局捕获异常即可实现:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String message = bindingResult.getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.fail(400, message);
}
@ExceptionHandler(ConstraintViolationException.class)
public Result handleConstraintException(ConstraintViolationException e) {
String message = e.getConstraintViolations().stream()
.map(v -> v.getMessage())
.collect(Collectors.joining("; "));
return Result.fail(400, message);
}
}内置注解覆盖不到的业务规则,比如手机号、身份证等特定格式,可以通过自定义注解扩展。先定义注解,再写一个校验器实现 ConstraintValidator 接口:
@Documented
@Constraint(validatedBy = PhoneValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
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 context) {
if (value == null) {
return true; // 空值交给 @NotNull 处理
}
return PATTERN.matcher(value).matches();
}
}使用时和内置注解完全一样,直接标注在字段上即可。自定义注解的好处是把校验规则收敛到一处,规则变更时只改一个类,比在各个接口里写正则判断干净得多。
整体来看,Spring Boot 整合 Validation 的成本很低,两个依赖加几行注解就能跑起来,再配合分组校验、级联校验和全局异常处理,可以覆盖绝大多数参数校验场景,值得在每个项目中落地。
Spring Boot Validation参数校验@Validated修改时间:2026-09-05 02:50:35