SpringBoot自定义注解校验的实现示例
作者:用户608308929047
一、为什么需要自定义注解校验?
Spring Boot 已经自带了一堆好用的校验注解,比如:
- @NotNull:不能为 null
- @Size(min=, max=):字符串 / 集合长度范围
- @Email:邮箱格式
- @Min / @Max:数值范围
但现实业务经常有"标准注解表达不了"的规则,比如:
- 手机号必须是 1 开头、11 位、第二位 3~9
- 密码必须包含大小写字母 + 数字
- 身份证号要符合校验规则
- 某个字段的值必须在枚举范围内
这些"业务规则",就可以用 自定义注解 + 校验器 来优雅地解决,让校验逻辑集中、可复用,而且写起来就像用 @NotNull 一样简单:
@Phone private String mobile;
二、核心三件套
自定义一个校验注解,本质上要准备三样东西:
| 角色 | 是什么 | 作用 |
|---|---|---|
| 注解 | 你自己定义的 @interface | 用在字段 / 参数上,声明"这里要校验" |
| @Constraint | 加在注解上的元注解 | 把注解和"校验器类"绑在一起 |
| ConstraintValidator | 一个实现类 | 写真正的校验逻辑(isValid) |
一句话记忆:注解负责"贴哪里、报什么错",校验器负责"怎么算合法"。
三、第一个例子:手机号校验
我们一步步来。
1. 加入依赖
Spring Boot 2.3 之后,校验被拆成了独立 starter,需要手动引入(如果是老版本可能已在 spring-boot-starter-web 里)。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
2. 创建自定义注解@Phone
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
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. 在实体类上使用
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 也行,区别见文末):
@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
@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读取参数
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;
}
}
使用:调用方自由指定范围
@Range(min = 18, max = 65, message = "年龄必须在 18 到 65 之间") private Integer age;
五、自定义更灵活的错误提示
有时候你想在提示里带出具体值,比如"18 不在 0~10 之间"。两种办法:
办法 A:占位符(推荐,配合国际化)
在注解的 message 里用 {} 引用注解属性:
String message() default "数值 {value} 不在允许范围内";
前提是注解里有对应属性 value(),或者你用 Bean Validation 内置的 ValidationMessages.properties 做键值映射。
办法 B:在代码里动态覆盖提示
@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),并加在类上开启方法级校验:
@Service
@Validated // 开启方法参数校验
public class UserService {
public void register(@Phone String mobile, @NotNull String name) {
// 如果 mobile 不合法,会抛 ConstraintViolationException
}
}
⚠️ 容易踩的点:@Valid 用在方法参数上一般只触发嵌套对象校验,@Validated 才能触发 方法参数 / 返回值 的校验。Service 层统一用 @Validated 最稳。
七、常见坑(初学者必看)
- 漏写 groups() / payload() :Bean Validation 规范要求注解里必须有这三个方法,少一个启动报错。直接照抄模板即可。
- isValid 里没处理 null:导致空值也报"格式错误",和 @NotNull 重复报错。约定俗成:null 交给 @NotNull,isValid 里直接 return true。
- @Constraint(validatedBy = ...) 写错类:编译不报错,但运行时校验器不生效,排查起来很懵。确认指向的 ConstraintValidator 实现类正确。
- 忘了加 spring-boot-starter-validation 依赖:Spring Boot 2.3+ 默认不带,没引的话 @Valid 完全不生效,还很安静地不报错。
- 注解的 @Retention 不是 RUNTIME:必须是运行时保留,否则框架读不到注解。
- 泛型类型不匹配:ConstraintValidator<注解, 字段类型>,比如字段是 String 就写 <Phone, String>,是 Integer 就写 <Range, Integer>,写错会编译/绑定失败。
- @Valid vs @Validated 混用:Controller 入参对象用 @Valid(@Validated 也行);Service 方法参数校验必须用 @Validated 且加在类上。
八、小结
自定义校验就三步:
- 写注解:加 @Constraint(validatedBy = XxxValidator.class),模板三件套 message/groups/payload 照抄,需要动态配置就加自定义属性。
- 写校验器:实现 ConstraintValidator<你的注解, 字段类型>,在 isValid 里写规则(null 返回 true),需要读参数就重写 initialize。
- 用起来:Controller 入参加 @Valid,Service 方法参数加 @Validated(类上也要加)。
掌握之后,你会发现原来 @Email、@Size 这些"官方注解"也是用同样的方式实现的——你现在已经能造自己的"官方级"注解了。🚀
到此这篇关于SpringBoot自定义注解校验的实现示例的文章就介绍到这了,更多相关SpringBoot自定义注解校验内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
