在构建 RESTful 接口时,参数校验是保障系统稳定性的第一道防线。如果将校验逻辑散落在 Service 层甚至业务代码深处,不仅会导致代码重复冗余,还会让接口职责变得模糊不清。Spring Boot 对 JSR-303 Bean Validation 规范提供了开箱即用的支持,通过声明式注解即可完成绝大多数校验场景,让开发者把精力集中在业务本身。

一、快速集成 Bean Validation 基础环境
Spring Boot 的 spring-boot-starter-web 起步依赖默认引入了 spring-boot-starter-validation,而后者又传递依赖了 Hibernate Validator,这是 JSR-303 规范的官方参考实现。也就是说,只要项目引入了 Web 起步依赖,就无需额外添加任何坐标,直接在实体类上使用校验注解即可生效。这一点对初学者来说非常友好,降低了集成门槛。
不过需要留意 Spring Boot 版本差异。在 2.3 之前的版本中,Web 起步依赖默认包含 validation;从 2.3 版本开始,官方将其从默认依赖中移除,需要手动添加 spring-boot-starter-validation。如果你的项目基于较新版本却突然发现注解不生效,大概率就是缺少了这个依赖。建议在 pom.xml 中显式声明,避免后续升级时出现隐患。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
依赖就绪后,还需要在配置类上添加 @Validated 注解或者直接在 Controller 方法参数前使用 @Validated 或 @Valid 来触发校验。两者的区别在于:@Valid 是 JSR-303 标准注解,不支持分组校验;@Validated 是 Spring 提供的增强注解,支持分组校验且可以标注在类级别。在实际项目中,推荐优先使用 @Validated,以便后续扩展分组逻辑。
二、常用校验注解与分组校验实战
JSR-303 提供了一系列内置约束注解,覆盖了绝大多数常见校验场景。@NotNull 检查对象不为 null;@NotBlank 检查字符串非 null 且去除首尾空格后长度大于零;@NotEmpty 检查集合、数组或字符串非 null 且长度大于零。这三者容易混淆,需要根据字段类型精确选择。此外还有 @Min、@Max 限定数值范围,@Size 限定集合或字符串长度,@Pattern 通过正则表达式约束格式,@Email 校验邮箱格式等。
下面通过一个用户注册的 DTO 来演示这些注解的组合使用。注意每个注解都通过 message 属性指定了自定义提示信息,当校验失败时,这些信息会被封装到异常对象中,方便后续统一处理。
public class UserRegisterDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3到20个字符之间")
private String username;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,20}$",
message = "密码必须包含字母和数字,长度8到20位")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 18, message = "年龄不能小于18岁")
@Max(value = 120, message = "年龄不能超过120岁")
private Integer age;
// 省略 getter 和 setter
}
在 Controller 中使用时,只需在参数前添加 @Validated 注解,Spring 的方法参数解析器就会在调用目标方法之前完成校验。如果校验失败,Spring 会抛出 MethodArgumentNotValidException,默认返回 400 状态码和一段冗长的 JSON 错误信息。这种默认行为对前端并不友好,后面会通过全局异常处理器来统一格式化。
@RestController
@RequestMapping("/api/user")
public class UserController {
@PostMapping("/register")
public Result<String> register(@Validated @RequestBody UserRegisterDTO dto) {
// 校验通过后才会执行到这里
userService.register(dto);
return Result.success("注册成功");
}
}
当同一个 DTO 在不同接口中需要应用不同校验规则时,分组校验就派上用场了。比如用户注册时不需要 userId,但更新时必须携带 userId。首先定义两个空接口作为分组标识,然后在注解上指定 groups 属性,最后在 Controller 的 @Validated 中声明使用哪个分组即可。
public interface RegisterGroup {}
public interface UpdateGroup {}
public class UserDTO {
@NotNull(groups = UpdateGroup.class, message = "用户ID不能为空")
private Long userId;
@NotBlank(groups = RegisterGroup.class, message = "用户名不能为空")
private String username;
// 省略其他字段
}
// 注册接口只校验 RegisterGroup
@PostMapping("/register")
public Result register(@Validated(RegisterGroup.class) @RequestBody UserDTO dto) { ... }
// 更新接口只校验 UpdateGroup
@PutMapping("/update")
public Result update(@Validated(UpdateGroup.class) @RequestBody UserDTO dto) { ... }
需要特别注意的是,一旦为某个字段指定了 groups 属性,该字段就不再属于默认分组 Default。这意味着如果你在 Controller 中使用不带分组参数的 @Validated,那些指定了 groups 的字段不会被校验。如果希望某个字段同时属于多个分组,可以在 groups 中同时声明,例如 @NotBlank(groups = {RegisterGroup.class, Default.class})。
三、自定义约束注解与全局异常处理
内置注解虽然丰富,但面对业务特定的校验需求时往往力不从心。比如校验手机号归属地、校验身份证校验位、校验枚举值是否合法等。这时就需要自定义约束注解。自定义注解的实现分为三步:定义注解接口、实现 ConstraintValidator 校验逻辑、在字段上使用注解。
下面以一个枚举校验注解为例,演示如何限制字段只能取特定值。这种场景在状态字段、类型字段中非常常见,比起在业务代码中写 if-else 判断要优雅得多。
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValidator.class)
public @interface EnumValid {
String[] values() default {};
String message() default "参数值不在允许范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class EnumValidator implements ConstraintValidator<EnumValid, String> {
private String[] allowedValues;
@Override
public void initialize(EnumValid constraintAnnotation) {
this.allowedValues = constraintAnnotation.values();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // null 由 @NotNull 负责
}
for (String allowed : allowedValues) {
if (allowed.equals(value)) {
return true;
}
}
return false;
}
}
使用时只需在字段上标注 @EnumValid 并传入允许值数组即可。当传入的值不在允许列表中时,校验器返回 false,框架自动抛出异常并携带 message 中定义的提示信息。这种声明式写法让校验逻辑与业务代码彻底解耦,复用性极强。
public class OrderDTO {
@EnumValid(values = {"PENDING", "PAID", "SHIPPED"}, message = "订单状态不合法")
private String status;
}
最后一步是统一处理校验异常。Spring 在校验失败时会抛出 MethodArgumentNotValidException(用于 @RequestBody 参数)或 ConstraintViolationException(用于方法级别参数校验)。如果不做处理,前端拿到的将是包含堆栈信息的原始错误响应,既不安全也不友好。通过 @RestControllerAdvice 配合 @ExceptionHandler,可以将这些异常拦截并转换为统一的响应格式。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationException(MethodArgumentNotValidException ex) {
// 获取第一个校验失败的字段及其提示信息
FieldError fieldError = ex.getBindingResult().getFieldError();
String message = fieldError != null ? fieldError.getDefaultMessage() : "参数校验失败";
return Result.fail(400, message);
}
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolation(ConstraintViolationException ex) {
String message = ex.getConstraintViolations()
.stream()
.findFirst()
.map(ConstraintViolation::getMessage)
.orElse("参数校验失败");
return Result.fail(400, message);
}
}
经过全局异常处理后,无论哪个接口触发了参数校验失败,前端都会收到结构一致的 JSON 响应,例如 code 为 400、message 为具体错误提示、data 为 null 的格式。这种统一格式不仅便于前端处理,也避免了敏感的内部异常信息泄露到客户端。在实际项目中,还可以结合日志框架记录校验失败的详细信息,方便排查问题。
除了 @RequestBody 参数校验,Spring 还支持对方法参数级别的校验,比如直接校验 @RequestParam 或 @PathVariable 传入的简单类型参数。使用方式是在 Controller 类上添加 @Validated 注解,然后在简单参数前直接使用 @NotNull、@Min 等注解即可。不过这种用法抛出的是 ConstraintViolationException 而非 MethodArgumentNotValidException,异常处理器中需要分别覆盖。
综合来看,JSR-303 Bean Validation 为 Spring Boot 项目提供了一套层次分明、扩展性强的参数校验体系。从内置注解到分组校验,再到自定义约束和全局异常处理,每一层都能独立演进而不影响其他部分。合理运用这套机制,可以让接口层代码保持简洁清晰,同时把校验逻辑收敛到一处,大幅降低维护成本。
Spring Boot参数校验Bean Validation修改时间:2026-08-30 04:02:53