当前位置: 首页 > news >正文

自定义Bean Validation注解并自定义校验逻辑

自定义校验注解

实现自定义校验注解,本质上是遵循JSR-303/JSR-380 (Bean Validation)规范。

在 Spring Boot 中实现它,只需要两步走

  1. 定义注解(接口):相当于制定法律条款。
  2. 定义校验器(实现类):相当于执法警察,负责执行逻辑。

我们以一个经典的实战场景为例:“校验某个字段不能包含空格”(命名为@NoSpace)。


第一步:定义注解 (@NoSpace)

我们需要创建一个注解接口,并在上面加上@Constraint注解来绑定它的“执行者”。

packagecom.example.common.validation;importjavax.validation.Constraint;importjavax.validation.Payload;importjava.lang.annotation.*;@Target({ElementType.FIELD,ElementType.PARAMETER})// 1. 作用在字段或参数上@Retention(RetentionPolicy.RUNTIME)// 2. 运行时生效@Documented// 3. 【核心】指定这就注解由哪个类来负责校验逻辑@Constraint(validatedBy=NoSpaceValidator.class)public@interfaceNoSpace{// 4. 默认错误提示信息Stringmessage()default"不能包含空格";// 5. 分组校验用(必须有)Class<?>[]groups()default{};// 6. 负载数据(必须有,虽然平时很少用)Class<?extendsPayload>[]payload()default{};}

第二步:定义校验器 (NoSpaceValidator)

这个类必须实现ConstraintValidator<注解, 字段类型>接口。
关键点:这个类会被 Spring 容器管理,所以你在里面可以使用@Resource注入 Service!

packagecom.example.common.validation;importjavax.validation.ConstraintValidator;importjavax.validation.ConstraintValidatorContext;// 泛型参数1:绑定的注解// 泛型参数2:要校验的数据类型 (这里是 String)publicclassNoSpaceValidatorimplementsConstraintValidator<NoSpace,String>{@Overridepublicvoidinitialize(NoSpaceconstraintAnnotation){// 初始化方法,如果注解里有参数(比如 max=10),可以在这里获取并存起来}/** * 校验逻辑的核心 * @param value 前端传进来的值 (比如 "zhang san") * @param context 上下文 * @return true=通过, false=失败 */@OverridepublicbooleanisValid(Stringvalue,ConstraintValidatorContextcontext){// 1. 最佳实践:如果是 null,直接返回 true。// 因为“非空校验”应该交给 @NotNull 去做,我们只负责“格式校验”。// 这样可以实现“选填,但填了就不能有空格”的效果。if(value==null){returntrue;}// 2. 校验逻辑:判断是否包含空格return!value.contains(" ");}}

第三步:在 VO 中使用

现在,你可以在你的UserSaveReqVO里使用它了,就像使用@NotNull一样。

@DatapublicclassUserSaveReqVO{@Schema(description="用户名")@NotBlank(message="用户名不能为空")// 标准注解@NoSpace(message="用户名里别搞空格,后端处理麻烦")// ✅ 你的自定义注解privateStringusername;}

第四步:在 Controller 中触发

这一步和普通校验一模一样,不需要做任何特殊修改。只要加上@Valid@Validated,Spring 就会自动发现你的自定义注解并执行校验器。

@PostMapping("/create")publicCommonResult<Long>createUser(@Valid@RequestBodyUserSaveReqVOreqVO){// 如果 username 是 "zhang san",这里进不来// 会抛出 MethodArgumentNotValidException// 你的 GlobalExceptionHandler 会捕获它,并返回 "用户名里别搞空格,后端处理麻烦"returnsuccess(userService.createUser(reqVO));}

🚀 进阶技巧:如何注入 Service 查询数据库?

自定义注解最强大的地方在于,校验器 (Validator) 是受 Spring 管理的 Bean。

场景:自定义一个@UniqueUsername注解,校验用户名是否已存在。

校验器写法:

publicclassUniqueUsernameValidatorimplementsConstraintValidator<UniqueUsername,String>{@Resource// ✅ 直接注入 Mapper 或 ServiceprivateUserMapperuserMapper;@OverridepublicbooleanisValid(Stringusername,ConstraintValidatorContextcontext){if(username==null)returntrue;// 直接查数据库UserDOuser=userMapper.selectByUsername(username);// 如果查到了,说明重复了,校验失败 (false)returnuser==null;}}

ConstraintValidatorContext 的作用

ConstraintValidatorContext是 Bean Validation(校验框架)提供给你的一个“控制台”“案情报告单”

isValid方法中,value是你要校验的嫌疑人,而context就是你手中的笔和罚单本

它主要有两大核心作用

  1. 改写错误文案:你不满意注解里默认写的message,想根据具体情况动态修改报错信息。
  2. 修改错误指向(重要):特别是在类级别校验(多字段联合校验)时,你想把错误精确地挂在某一个具体的字段上,而不是挂在整个对象上。

下面我通过两个具体的实战场景来演示。


场景一:动态修改错误提示 (改写文案)

需求
你写了一个@CheckPassword注解。

  • 如果密码太短,你想报:“密码长度不够”。
  • 如果密码太简单,你想报:“密码必须包含大写字母”。
  • 但是注解上的message只能写死一句话。

**怎么做?**利用context禁用默认提示,手写新的提示。

publicclassPasswordValidatorimplementsConstraintValidator<CheckPassword,String>{@OverridepublicbooleanisValid(Stringpassword,ConstraintValidatorContextcontext){if(password==null)returntrue;// 1. 检查长度if(password.length()<6){// 【核心动作】// A. 禁用掉注解里默认定义的 messagecontext.disableDefaultConstraintViolation();// B. 手动添加一条新的错误信息context.buildConstraintViolationWithTemplate("密码长度不能少于6位").addConstraintViolation();returnfalse;}// 2. 检查复杂度if(!password.contains("@")){// 【核心动作】context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate("密码必须包含特殊字符 @").addConstraintViolation();returnfalse;}returntrue;}}

场景二:类级别校验,指哪打哪 (修改指向)

这是ConstraintValidatorContext最不可替代的作用!

需求
你要校验UserReqVO中的passwordconfirmPassword是否一致。
这是一个跨字段的逻辑,所以注解@PasswordMatch必须加在类(Class)上,而不是字段上。

问题
如果你直接返回false,Spring 会认为错误发生在UserReqVO这个对象上,而不是confirmPassword这个字段上。
前端收到错误时,无法把红框标在“确认密码”的输入框旁边,用户体验很差。

怎么做?利用context把错误“重定向”到具体字段。

VO 代码:

@PasswordMatch// 注解加在类上publicclassUserRegisterReqVO{privateStringpassword;privateStringconfirmPassword;}

校验器代码:

publicclassPasswordMatchValidatorimplementsConstraintValidator<PasswordMatch,UserRegisterReqVO>{@OverridepublicbooleanisValid(UserRegisterReqVOvo,ConstraintValidatorContextcontext){if(vo.getPassword()==null||vo.getConfirmPassword()==null){returntrue;}// 如果不一致if(!vo.getPassword().equals(vo.getConfirmPassword())){// 1. 禁用默认报错(因为它会报在 UserRegisterReqVO 对象头上)context.disableDefaultConstraintViolation();// 2. 【核心黑科技】建立一个新的报错,并指向 confirmPassword 字段context.buildConstraintViolationWithTemplate("两次输入的密码不一致")// 设置文案.addPropertyNode("confirmPassword")// 指向字段名!.addConstraintViolation();// 完成添加returnfalse;}returntrue;}}

效果:
前端收到的 JSON 中,错误的 key 不再是userRegisterReqVO,而是精确的confirmPassword

http://www.cnnetsun.cn/news/115793.html

相关文章:

  • 拒绝“魔法值”注入:手把手教你实现 Spring Boot 高性能枚举校验注解 @InEnum
  • 国内容易上手的claudecode一键配置指南
  • 复原IP地址
  • Redis 发布订阅
  • JQuery支持WebUploader完成百万文件断点续传的原理?
  • Vue3如何结合组件实现大文件分片的并行上传优化?
  • 类型分布统计-Cordovaopenharmony多维分析实战
  • 四时四名,一山万象:朝鲜金刚山的锦绣风姿
  • 基于Spring Boot的果蔬销售系统
  • Scala Collection(集合)
  • 介观交通流仿真软件:DynusT_(11).交通事件管理
  • django基于Python天气分析系统
  • python基于大数据的分析长沙旅游景点推荐系统
  • 基于Django的学分管理系统
  • 广度优先遍历与最短路径
  • 通信系统仿真:通信系统基础理论_(11).光通信技术
  • 17、Linux文件与目录操作全解析
  • 21、Linux系统进程与包管理全解析
  • 二叉排序树的插入、先序/中序/后序/层次遍历、节点查询
  • 如何在 Spring Boot 中接入 Amazon ElastiCache
  • 基于51单片机的血糖步数测量仪
  • Linux C/C++ 学习日记(51):内存池
  • AAAI25|基于神经共形控制的时间序列预测模型
  • CATCH:ICLR 2025 最值得关注的时间序列异常检测新框架
  • 开发到生产全链路:Docker containerd Kubernetes 运行时全景指南
  • 文件包含漏洞终极指南
  • #扫雷游戏
  • Java计算机毕设之基于springboot+vue的高校学院校内订餐系统的设计与实现基于JAVA的学院校内订餐系统的实现(完整前后端代码+说明文档+LW,调试定制等)
  • 小程序计算机毕设之基于微信跑腿小程序的设计与实现基于springboot+微信小程序的跑腿小程序的设计与实现(完整前后端代码+说明文档+LW,调试定制等)
  • 小程序计算机毕设之基于springboot+微信小程序的餐厅预约系统设计与实现基于微信小程序的餐厅预约系统设计与实现(完整前后端代码+说明文档+LW,调试定制等)