学完这篇,你能在 Spring Boot 项目里用 Bean Validation 完成参数校验,并写出一个可复用的自定义校验注解(含校验器和默认提示语),不依赖手写 if 判断。
第一步:引入依赖
Spring Boot 2.3 之后校验 starter 已从 web 里拆出,需要单独加:
<!-- Spring Boot 3.x -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Boot 2.x 项目里注解包名是 javax.validation.*,Boot 3.x 改成了 jakarta.validation.*,混用会直接报 NoClassDefFoundError,升级时注意全量替换 import。
第二步:在 DTO 上写约束注解
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度需在 2-20 之间")
private String username;
@NotNull
@Min(value = 18, message = "年龄不能小于 18")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
}
Controller 上加 @Valid(或 @Validated)才会触发校验:
@PostMapping("/users")
public Result create(@Valid @RequestBody UserCreateDTO dto) { ... }
注意:@Valid 加在 Controller 方法参数上才生效,加在 DTO 类上没用。校验失败默认抛 MethodArgumentNotValidException,建议用 @RestControllerAdvice 统一 catch 后返回第一个 FieldError 的 message,别把整段异常栈丢给前端。
第三步:处理集合与嵌套对象
嵌套对象必须在内层字段上再标一次 @Valid,否则只校验外层、内层字段全部放过:
public class OrderDTO {
@Valid
@NotNull
private UserCreateDTO buyer; // 不加 @Valid,buyer 内部注解全部失效
@Valid
private List<ItemDTO> items; // 集合元素同样需要
}
对方法参数逐个校验(不是对象)时,把 @Valid 换成类上的 @Validated,参数上直接标注解:
@Validated
@RestController
public class UserController {
public Result get(@Min(1) @RequestParam Long id) { ... }
}
第四步:写自定义注解
内置注解不够用时,做两步:定义注解 + 实现 ConstraintValidator。以「手机号」为例:
@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
message、groups、payload 三个方法是 Bean Validation 规范强制要求的,缺一个启动时就报错。
第五步:实现校验器
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern P = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (value == null || value.isEmpty()) {
return true; // null 交给 @NotBlank 管,这里放行
}
return P.matcher(value).matches();
}
}
用法:字段上写 @Phone 即可,和其他注解叠加使用。
注意:isValid 返回 true 表示校验通过,很多人会写反。另外校验器默认对 null 放行——这是规范行为,不是 bug,空值请用 @NotNull / @NotBlank 单独约束。校验器实例是单例,不要在里面存请求级状态。
第六步:让提示语带上参数
想让 message 支持「最小值 {value}」这种动态文案,在校验器里关掉默认提示:
@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (ok(value)) return true;
ctx.disableDefaultConstraintViolation();
ctx.buildConstraintViolationWithTemplate("手机号 " + value + " 不存在")
.addConstraintViolation();
return false;
}
注意:注解声明处不能写 @Target(ElementType.TYPE) 之外的随意组合,字段、参数、方法返回值按实际需要显式列出;@Repeatable 场景需额外声明 List 容器注解。
小结
- 依赖:Boot 2.3+ 必须单独加
spring-boot-starter-validation,注意 javax / jakarta 包名差异。
- 触发:对象校验用
@Valid,方法参数逐个校验用类上 @Validated。
- 嵌套:内层对象和集合元素都要补
@Valid,否则静默失效。
- 自定义注解:注解上必须带
@Constraint 和 message/groups/payload 三件套;校验器返回 true 表示通过,null 默认放行。
- 校验器无状态、单例,别塞成员变量;提示语走
message 或 buildConstraintViolationWithTemplate。