SpringBoot自定义注解校验

一、为什么需要自定义注解校验?

Spring Boot 已经自带了一堆好用的校验注解,比如:

  • @NotNull:不能为 null
  • @Size(min=, max=):字符串 / 集合长度范围
  • @Email:邮箱格式
  • @Min / @Max:数值范围

但现实业务经常有"标准注解表达不了"的规则,比如:

  • 手机号必须是 1 开头、11 位、第二位 3~9
  • 密码必须包含大小写字母 + 数字
  • 身份证号要符合校验规则
  • 某个字段的值必须在枚举范围内

这些"业务规则",就可以用 自定义注解 + 校验器 来优雅地解决,让校验逻辑集中、可复用,而且写起来就像用 @NotNull 一样简单:

typescript 复制代码
@Phone
private String mobile;

二、核心三件套

自定义一个校验注解,本质上要准备三样东西:

角色 是什么 作用
注解 你自己定义的 @interface 用在字段 / 参数上,声明"这里要校验"
@Constraint 加在注解上的元注解 把注解和"校验器类"绑在一起
ConstraintValidator 一个实现类 写真正的校验逻辑(isValid

一句话记忆:注解负责"贴哪里、报什么错",校验器负责"怎么算合法"。

三、第一个例子:手机号校验

我们一步步来。

1. 加入依赖

Spring Boot 2.3 之后,校验被拆成了独立 starter,需要手动引入(如果是老版本可能已在 spring-boot-starter-web 里)。

xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>

2. 创建自定义注解 @Phone

java 复制代码
 package com.example.demo.validation;  
 ​  
 import javax.validation.Constraint;  
 import javax.validation.Payload;  
 import java.lang.annotation.*;  
 ​  
 @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 规范强制要求 的,少一个都会在启动时报错。所以新建注解时直接照抄这三行最省心。

3. 创建校验器 PhoneValidator

java 复制代码
 package com.example.demo.validation;  
 ​  
 import javax.validation.ConstraintValidator;  
 import javax.validation.ConstraintValidatorContext;  
 import java.util.regex.Pattern;  
 ​  
 public class PhoneValidator implements ConstraintValidator<Phone, String> {  
 ​  
     // 中国大陆手机号正则:1 开头,第二位 3~9,共 11 位  
     private static final Pattern PATTERN = Pattern.compile("^1[3-9]\d{9}$");  
 ​  
     /**  
      * 第一个泛型 <Phone>  :对应的注解类型  
      * 第二个泛型 <String> :被校验字段的类型  
      */  
     @Override  
     public boolean isValid(String value, ConstraintValidatorContext context) {  
         // 重点:遇到 null 直接返回 true  
         // 原因:null 应该交给 @NotNull 去管,避免"空值 + 格式错"双重报错  
         if (value == null) {  
             return true;  
         }  
         return PATTERN.matcher(value).matches();  
     }  
 }

isValid 返回 true 表示通过,false 表示不通过、会触发 message() 里的错误提示。

4. 在实体类上使用

kotlin 复制代码
 package com.example.demo.dto;  
 ​  
 import com.example.demo.validation.Phone;  
 import javax.validation.constraints.NotNull;  
 ​  
 public class UserDTO {  
 ​  
     @NotNull(message = "姓名不能为空")  
     private String name;  
 ​  
     @Phone  
     private String mobile;  
 ​  
     // getter / setter 省略  
 }

5. 在 Controller 里触发校验

关键是在入参上加 @Valid (Spring 的 @Validated 也行,区别见文末):

less 复制代码
 @RestController  
 @RequestMapping("/users")  
 public class UserController {  
 ​  
     @PostMapping  
     public String create(@Valid @RequestBody UserDTO user) {  
         return "校验通过,创建用户:" + user.getName();  
     }  
 }

当传入的 mobile 不是合法手机号时,Spring 会自动返回 400,并在错误体里带上 "手机号格式不正确"

小提示:如果你想在前端看到结构化错误信息,可以用 @ExceptionHandler(MethodArgumentNotValidException.class) 捕获并统一返回格式,初学阶段先知道"会报错"即可。


四、进阶:让注解支持"参数"(动态配置)

上面 @Phone 规则是写死的。如果想做一个"数值范围"校验,允许调用方指定 min / max,怎么办?

很简单:在注解里加普通方法(不是 message/groups/payload 那三个),然后在校验器的 initialize 方法里读取。

注解:加 min / max

less 复制代码
 @Target({ElementType.FIELD})  
 @Retention(RetentionPolicy.RUNTIME)  
 @Constraint(validatedBy = RangeValidator.class)  
 public @interface Range {  
 ​  
     String message() default "数值超出允许范围";  
 ​  
     int min() default 0;   // 自定义参数  
     int max() default 100; // 自定义参数  
 ​  
     Class<?>[] groups() default {};  
     Class<? extends Payload>[] payload() default {};  
 }

校验器:用 initialize 读取参数

arduino 复制代码
 public class RangeValidator implements ConstraintValidator<Range, Integer> {  
 ​  
     private int min;  
     private int max;  
 ​  
     // 初始化时,把注解上的 min/max 读进来  
     @Override  
     public void initialize(Range annotation) {  
         this.min = annotation.min();  
         this.max = annotation.max();  
     }  
 ​  
     @Override  
     public boolean isValid(Integer value, ConstraintValidatorContext context) {  
         if (value == null) {  
             return true;  
         }  
         return value >= min && value <= max;  
     }  
 }

使用:调用方自由指定范围

arduino 复制代码
 @Range(min = 18, max = 65, message = "年龄必须在 18 到 65 之间")  
 private Integer age;

五、自定义更灵活的错误提示

有时候你想在提示里带出具体值,比如"18 不在 0~10 之间"。两种办法:

办法 A:占位符(推荐,配合国际化)

在注解的 message 里用 {} 引用注解属性:

arduino 复制代码
 String message() default "数值 {value} 不在允许范围内";

前提是注解里有对应属性 value(),或者你用 Bean Validation 内置的 ValidationMessages.properties 做键值映射。

办法 B:在代码里动态覆盖提示

arduino 复制代码
 @Override  
 public boolean isValid(Integer value, ConstraintValidatorContext context) {  
     if (value != null && (value < min || value > max)) {  
         // 关闭默认提示  
         context.disableDefaultConstraintViolation();  
         // 动态生成提示并添加  
         context.buildConstraintViolationWithTemplate(  
                 "数值 " + value + " 不在 " + min + "~" + max + " 之间")  
                .addConstraintViolation();  
         return false;  
     }  
     return true;  
 }

六、在 Service 方法参数上校验(不止 Controller)

除了 Controller 的 @RequestBody,你还经常想在 Service 方法入参上直接校验。这时要用 @Validated (Spring 提供的,不是 javax.validation@Valid),并加在类上开启方法级校验:

less 复制代码
 @Service  
 @Validated // 开启方法参数校验  
 public class UserService {  
 ​  
     public void register(@Phone String mobile, @NotNull String name) {  
         // 如果 mobile 不合法,会抛 ConstraintViolationException  
     }  
 }

⚠️ 容易踩的点:@Valid 用在方法参数上一般只触发嵌套对象校验,@Validated 才能触发 方法参数 / 返回值 的校验。Service 层统一用 @Validated 最稳。


七、常见坑(初学者必看)

  1. 漏写 groups() / payload() :Bean Validation 规范要求注解里必须有这三个方法,少一个启动报错。直接照抄模板即可。
  2. isValid 里没处理 null :导致空值也报"格式错误",和 @NotNull 重复报错。约定俗成:null 交给 @NotNullisValid 里直接 return true
  3. @Constraint(validatedBy = ...) 写错类 :编译不报错,但运行时校验器不生效,排查起来很懵。确认指向的 ConstraintValidator 实现类正确。
  4. 忘了加 spring-boot-starter-validation 依赖 :Spring Boot 2.3+ 默认不带,没引的话 @Valid 完全不生效,还很安静地不报错。
  5. 注解的 @Retention 不是 RUNTIME:必须是运行时保留,否则框架读不到注解。
  6. 泛型类型不匹配ConstraintValidator<注解, 字段类型>,比如字段是 String 就写 <Phone, String>,是 Integer 就写 <Range, Integer>,写错会编译/绑定失败。
  7. @Valid vs @Validated 混用 :Controller 入参对象用 @Valid@Validated 也行);Service 方法参数校验必须用 @Validated 且加在类上。

八、小结

自定义校验就三步:

  1. 写注解 :加 @Constraint(validatedBy = XxxValidator.class),模板三件套 message/groups/payload 照抄,需要动态配置就加自定义属性。
  2. 写校验器 :实现 ConstraintValidator<你的注解, 字段类型>,在 isValid 里写规则(null 返回 true),需要读参数就重写 initialize
  3. 用起来 :Controller 入参加 @Valid,Service 方法参数加 @Validated(类上也要加)。

掌握之后,你会发现原来 @Email@Size 这些"官方注解"也是用同样的方式实现的------你现在已经能造自己的"官方级"注解了。🚀

相关推荐
董员外1 小时前
RAG 系统进化论(六):GraphRAG(基于知识图谱的 RAG),从相似文本走向实体关系
人工智能·后端·设计模式
Conan在掘金1 小时前
ArkTS 进阶之道(31):状态联动深水区收官边界——为啥四级状态容器 + V1/V2 双轨是状态联动深水区收官根因
后端
Conan在掘金1 小时前
鸿蒙 7.0 空间美学开篇:沉浸式毛玻璃 + 底部 Sheet 面板——一行 backgroundBlurStyle 玩出物理材质感
后端
Java编程爱好者1 小时前
R2DBC vs JDBC:Spring Boot 响应式项目该怎么选
后端
AskHarries1 小时前
Sitemap 怎么自动生成
后端
神奇小汤圆1 小时前
深入 Java 线程池:从源码原理、生产调优到故障排查全链路指南
后端
Conan在掘金2 小时前
ArkTS 进阶之道(30):@Track 精准观测边界——为啥 class 属性级观测只刷关联 UI 根因
后端
明月_清风2 小时前
🚀 OpenAI 数据代理架构全解析:从 600 PB 到自然语言的六层上下文工程
前端·后端·架构
明月_清风2 小时前
🚀 从 Foundry 到 AIP:Palantir 发生了什么变化?一篇文章全搞懂
前端·后端