news 2026/6/10 20:56:47

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自定义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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/9 19:15:14

线程的常用方法

目录 1. 获取当前线程 - Thread.currentThread() 2. 线程命名 - setName() 和 getName() 3. 线程休眠 - Thread.sleep() 4. 线程插队 - join() 5. 启动线程 - start() 6. 中断线程 - interrupt() 7. 线程优先级 - setPriority() 和 getPriority() 8. 守护线程 - setDa…

作者头像 李华
网站建设 2026/6/10 11:34:57

湖南网安培训首选:湖南省网安基地深度解析

想在湖南学网络安全&#xff1f;这家机构你必须了解——国家级资质、政企资源、实战导向的行业标杆&#x1f3c6; 一、官方权威认证&#xff1a;为什么是湖南省网安基地&#xff1f;1.1 国家级战略定位湖南省网安基地承载着重要的国家战略使命&#xff0c;拥有多项国家级和省级…

作者头像 李华
网站建设 2026/6/10 20:14:26

ComponentOne Studio Enterprise 2025 v2

使用 WPF 的 2D 等高线图可视化 3D 数据2025年12月12日ComponentOne Studio Enterprise 2025 v2 通过高性能的轮廓映射和渐变可视化功能提升您的 WPF 应用程序。ComponentOne Studio Enterprise 是由 MESCIUS&#xff08;前身为 GrapeCity&#xff09;开发的一套全面的 .NET UI…

作者头像 李华
网站建设 2026/6/10 12:27:13

产品的机械结构对EMC的影响

大家好,欢迎来到“电子工程师之家”,大家也可以关注微信公众号同号“电子工程师之家”。微信公众号中有更多精彩内容。 你有没有遇到过这些“EMC玄学”问题? 电路板单独测试没问题,装进外壳后辐射发射突然超标; 设备在实验室好好的,一到客户现场就被强电磁环境“搞死机…

作者头像 李华
网站建设 2026/6/9 23:18:57

Vue-cli如何集成JQuery完成网页大文件秒传功能?

大文件传输系统设计方案&#xff08;基于SM4国密算法&#xff09; 需求分析 作为四川某软件公司的开发人员&#xff0c;我面临以下核心需求&#xff1a; 实现10GB级别大文件的分片上传/下载采用国密SM4算法进行端到端加密服务端需支持SM4加密存储兼容主流浏览器及信创国产化…

作者头像 李华
网站建设 2026/6/10 13:25:27

智慧零售新视界:基于Rokid Glasses的AR智能导购系统深度实现

本文深入探讨如何利用Rokid CXR-M SDK开发一套完整的AR智能导购系统&#xff0c;通过眼镜端实时拍照识别商品&#xff0c;结合自定义UI界面展示促销信息与用户评价。文章从SDK架构分析入手&#xff0c;详细阐述蓝牙/Wi-Fi双模连接机制、图像识别集成方案、自定义UI开发技巧&…

作者头像 李华