尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

ValidX分组验证详解:不同场景使用不同验证规则

ValidX分组验证详解:不同场景使用不同验证规则 ValidX分组验证详解不同场景使用不同验证规则引言一个 DTO 引发的连环事故创建时必填、更新时禁止UserDTO里用户名注册时必须填、编辑资料时却不用改ID 新增时不能传、修改时必须带。于是团队拆了CreateUserDTO和UpdateUserDTO两个类字段复制一份改字段时漏改一处线上悄悄出现更新把用户名清空了的 bug。同一字段两套规则订单号在下单和查询两个接口里一个必填、一个可空。为了一个字段写两个 DTOController 里到处是if (dto.getX() ! null)的补丁式判断。审核流程校验崩溃运营后台把草稿状态的商品直接提交审核DTO 里审核人必须存在的规则把运营卡死——因为那条规则在草稿阶段本不该执行。这三个问题的根源是同一个一份数据在生命周期不同阶段需要满足的规则不一样却只有一套验证。ValidX 基于 Jakarta Bean Validation 规范项目实际使用javax.validation 2.0.1Hibernate Validator 6.1.5所有注解原生支持标准分组机制。本文对照源码讲透分组验证的完整用法从分组是什么到Default 组/组继承/GroupSequence的进阶玩法再落到 Spring Boot 与链式 API 的边界最后给出可直接照抄的全场景 DTO。说明文中 ValidX 相关内容对照 v1.2.0 源码全部 100 注解的groups属性、chain/ValidX.java链式实现与测试用例HourMinuteSecondValidatorTest、NotContainsValidatorTest等使用Validation.buildDefaultValidatorFactory().getValidator().validate(...)的标准触发方式核实。一、为什么需要分组验证1.1 一套数据多种身份同一个业务对象在不同阶段规则不同场景对象典型规则差异创建新数据必填字段最多、ID 必须为空更新存量数据ID 必须存在、部分字段禁止修改审核提审数据只有审核相关字段被校验查询查询条件全部可空只校验填了的格式没有分组时要么拆 DTO类爆炸、字段重复维护要么写if补丁校验逻辑散落、容易漏。1.2 分组验证的本质分组验证是 Bean Validation 规范的标准机制核心就三件事1. 定义分组接口一个空接口即可 2. 注解上用 groups 指定它属于哪个分组 3. 触发验证时传入要执行的分组ValidX 的每个注解都原生带groups()属性与标准注解NotBlank、Size等完全混用不需要任何额外配置。看一个真实源码中的注解定义annotations/ChineseName.javaConstraint(validatedByChineseNameValidator.class)publicinterfaceChineseName{Stringmessage()default{io.github.vipxieliang.validx.annotation.chinese.name};Class?[]groups()default{};// ← 分组每个 ValidX 注解都有Class?extendsPayload[]payload()default{};}groups是标准的Class?[]数组——一个约束可以同时属于多个分组。二、三分钟上手定义分组并用起来2.1 定义分组分组就是空接口没有方法、没有继承约束纯粹是标记。习惯上作为嵌套接口放在 DTO 里就近管理publicclassUserDTO{/** 创建场景 */publicinterfaceCreateGroup{}/** 更新场景 */publicinterfaceUpdateGroup{}// 以下字段按场景挂分组 Null(groupsCreateGroup.class,message创建时 ID 必须为空)NotNull(groupsUpdateGroup.class,message更新时 ID 不能为空)privateLongid;NotBlank(groupsCreateGroup.class,message创建时用户名必填)Size(min2,max20,message用户名长度 2~20)privateStringusername;EmailprivateStringemail;}注意email字段没写 groups——它属于默认的Default组详见第三章不限定场景时所有场景都校验。2.2 触发验证传入分组非 Spring 环境工具类、单元测试、服务层手动校验用标准 Bean Validation 入口importjavax.validation.Validation;importjavax.validation.Validator;importjavax.validation.ConstraintViolation;ValidatorvalidatorValidation.buildDefaultValidatorFactory().getValidator();// 只执行 CreateGroup 分组的约束SetConstraintViolationUserDTOviolationsvalidator.validate(dto,UserDTO.CreateGroup.class);if(!violations.isEmpty()){Stringmsgviolations.iterator().next().getMessage();// 创建时id 必须为 null、用户名必填……email 的 Email 也生效}validate(obj, Class?... groups)支持传多个分组用逗号并列即可// 同时执行 CreateGroup 和 UpdateGroupvalidator.validate(dto,UserDTO.CreateGroup.class,UserDTO.UpdateGroup.class);关键行为传入的分组决定哪些约束执行。没传分组validator.validate(dto)等价于只传Default组。2.3 对照源码这就是 ValidX 测试的调用方式分组验证不是 ValidX 发明的但 ValidX 的注解要能被validate(dto, groups...)驱动前提是每个注解都规范实现了message/groups/payload。这一点在仓库测试里可以验证——所有注解测试都用同一套标准入口// 摘自 ValidX 测试用例如 HourMinuteSecondValidatorTestValidatorFactoryfactoryValidation.buildDefaultValidatorFactory();Validatorvalidatorfactory.getValidator();SetConstraintViolationTestModelviolationsvalidator.validate(model);把validate(model)换成validate(model, XxxGroup.class)就是分组验证。注解方式的分组支持是天然可用的0 配置。三、Default 组没写 groups 的注解去哪了3.1 隐式归属不写groups的约束隐式属于javax.validation.groups.Default组。这是最容易踩的坑publicclassUserDTO{NotBlank(groupsCreateGroup.class,message创建时必填)privateStringusername;Email// 属于 Default 组privateStringemail;}调用validate(dto, CreateGroup.class)时只有 username 的 NotBlank 执行email 的 Email 不执行——因为触发的是CreateGroup而Email挂在Default上。3.2 三个判定规则调用方式执行的约束说明validate(dto)只执行Default组未标 groups 的约束全部执行validate(dto, CreateGroup.class)只执行CreateGroup未标 groups 的约束全部跳过validate(dto, Default.class, CreateGroup.class)DefaultCreateGroup传多个组即可合并也就是说一旦指定了任意非 Default 分组那些忘了写 groups的约束就会悄悄失效。这是分组验证最常见的看起来没生效的原因。3.3 让分组包含默认规则希望用 CreateGroup 时默认规则也执行让分组接口继承 DefaultpublicclassUserDTO{/** 让 CreateGroup 包含默认规则 */publicinterfaceCreateGroupextendsDefault{}}这样validate(dto, CreateGroup.class)会同时执行CreateGroup组约束和Default组约束。命名上更推荐CreateGroup extends Default而不是反过来语义是创建时包含通用规则。四、组继承子组天然拥有父组规则分组接口之间可以互相继承验证子组时父组的约束也一并执行publicclassUserDTO{/** 基础规则所有写操作都要校验 */publicinterfaceBasicGroup{}/** 创建 基础 创建专属 */publicinterfaceCreateGroupextendsBasicGroup{}/** 更新 基础 更新专属 */publicinterfaceUpdateGroupextendsBasicGroup{}NotBlank(groupsBasicGroup.class,message用户名不能为空)Size(min2,max20,message用户名长度 2~20)privateStringusername;Null(groupsCreateGroup.class,message创建时 ID 必须为空)NotNull(groupsUpdateGroup.class,message更新时 ID 不能为空)privateLongid;}行为触发分组执行的约束BasicGroupusername 必填/长度CreateGroupusername 规则 id 必须为 nullUpdateGroupusername 规则 id 不能为 null继承解决的是公共规则只写一次把每写必用的约束挂到父组各场景组继承它不用在子组里重复标注。五、GroupSequence控制验证顺序与短路5.1 为什么需要顺序默认情况下分组验证无序全部约束都跑一遍。但有些场景要求先轻后重先做格式校验字段格式对不对再做业务校验值在不在合理范围——格式都错了业务校验白算先做成本低的校验再做成本高的校验如查库。GroupSequence让分组按指定顺序验证前一个分组有约束失败后面分组直接跳过短路。importjavax.validation.GroupSequence;/** 先校验基础格式再校验业务规则 */GroupSequence({BasicGroup.class,BusinessGroup.class})publicclassUserDTO{publicinterfaceBasicGroup{}publicinterfaceBusinessGroup{}NotBlank(groupsBasicGroup.class,message用户名不能为空)privateStringusername;NotNull(groupsBusinessGroup.class,message用户名不能重复)privateStringusernameUniqueCheck;// 业务校验示例实际校验逻辑由自定义验证器承担}注意顺序组只有在验证Default组时才生效——规范规定触发顺序组的正确姿势是把它作为Default组的定义类上标注的GroupSequence会把Default组重定义为序列所以最稳妥的写法是把默认组加进序列尾部GroupSequence({BasicGroup.class,BusinessGroup.class,Default.class})publicclassUserDTO{...}验证时照常validate(dto, Default.class)等价于validate(dto)就会按BasicGroup → BusinessGroup → Default的顺序执行前一组的约束有失败后面的组直接跳过。5.2 实用建议GroupSequence适合同一字段上格式 → 业务分级以及多字段校验成本差异大的场景。如果只是创建/更新规则不同用普通分组即可不要为用而用。六、分组验证在 Spring Boot 中的落地6.1 Validated 指定分组Spring 里区分两类注解注解支持分组适用Valid❌无分组的常规校验、嵌套对象递归Validated✅需要指定分组时写在方法参数上RestControllerpublicclassUserController{PostMapping(/users)publicResultcreate(Validated(UserDTO.CreateGroup.class)RequestBodyUserDTOdto){returnuserService.create(dto);}PutMapping(/users/{id})publicResultupdate(Validated(UserDTO.UpdateGroup.class)RequestBodyUserDTOdto){returnuserService.update(dto);}}坑点Controller 方法参数上用了Validated后类上的Validated就不必再写方法参数级Validated(分组)只对该参数生效。6.2 嵌套对象的分组ConvertGroup嵌套对象默认沿用外层分组需要把外层分组映射成内层分组时用ConvertGrouppublicclassOrderDTO{NotNull(groupsSubmitGroup.class,message收货地址不能为空)ValidConvertGroup(fromSubmitGroup.class,toAddressDTO.SubmitGroup.class)privateAddressDTOaddress;}6.3 统一异常处理分组验证失败同样抛MethodArgumentNotValidException参数校验失败与无分组完全一致全局异常处理器无需改动RestControllerAdvicepublicclassGlobalExceptionHandler{ExceptionHandler(MethodArgumentNotValidException.class)publicResultVoidhandleValidation(MethodArgumentNotValidExceptione){Stringmsge.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage).collect(Collectors.joining());returnResult.fail(400,msg);}}七、ValidX 的能力边界注解支持分组链式 API 不支持7.1 两种模式的分组支持对比这是理解 ValidX 必须记住的分工能力注解方式链式 API分组验证✅groups原生支持❌ 无分组概念触发方式validate(dto, Group.class)/Validated(Group.class)field(...).isXxx(...)全部当场执行适用DTO 数据契约、多场景复用动态数据、临时校验、非 Spring 环境对照源码可以确认ValidX 全部注解ChineseName、ChineseIdCard、PhoneNumber、Email、Date、Timestamp……100 个都实现了标准的groups()/payload()而链式入口chain/ValidX.java中没有任何 group 相关方法——isValid()就是全部校验结果不支持按分组选择性执行。7.2 链式 API 需要分组怎么办链式 API 没有分组两种替代方案方案一职责拆分。把必填/格式这类场景差异放到注解 DTO 上走分组链式 API 只做流程内的临时校验各司其职// 注解 DTO分组管什么场景校验什么// 链式 API管动态数据当场校验ValidXvalidatorValidX.init().field(回调时间戳).isTimestamp(notify.getTimestamp(),Timestamp.TimestampUnit.SECONDS);方案二if 分支模拟。业务场景本身可以用分支表达if(isCreate){validator.field(用户名).isRequired(username);}else{validator.field(用户名).isAlphaNum(username);}两种方案都不完美但足够清晰。凡是同一 DTO 多场景复用优先用注解方式 分组——这正是本文的适用面。八、实战用户资料 DTO 全场景覆盖把前面的知识串起来写一个覆盖注册 / 更新 / 审核三个场景的完整 DTOimportjavax.validation.Valid;importjavax.validation.constraints.*;importjavax.validation.groups.Default;importio.github.vipxieliang.validx.annotations.*;publicclassUserDTO{// 分组定义 /** 公共规则所有写操作 */publicinterfaceBasicGroup{}/** 注册 */publicinterfaceCreateGroupextendsBasicGroup,Default{}/** 更新资料 */publicinterfaceUpdateGroupextendsBasicGroup,Default{}/** 运营审核 */publicinterfaceAuditGroupextendsDefault{}// 字段 Null(groupsCreateGroup.class,message注册时 ID 必须为空)NotNull(groupsUpdateGroup.class,message更新时 ID 不能为空)privateLongid;NotBlank(groupsBasicGroup.class,message用户名不能为空)Size(min2,max20,message用户名长度 2~20)AlphaNum// ValidX字母数字privateStringusername;Email// Default 组所有场景都校验格式privateStringemail;NotBlank(groupsCreateGroup.class,message注册时密码必填)Password(minLength8,groupsCreateGroup.class,message密码至少 8 位)privateStringpassword;PhoneNumber// Default 组有值才校验格式privateStringphone;NotBlank(groupsAuditGroup.class,message审核意见不能为空)Size(max200,message审核意见最多 200 字)privateStringauditRemark;}三个场景触发// 注册创建专属 公共 默认email 格式、phone 格式也校验validator.validate(dto,UserDTO.CreateGroup.class);// 更新更新专属 公共 默认validator.validate(dto,UserDTO.UpdateGroup.class);// 审核只校验审核意见 默认格式validator.validate(dto,UserDTO.AuditGroup.class);这个 DTO 在 Spring 中对应三个接口PostMapping(/register)publicResultregister(Validated(UserDTO.CreateGroup.class)RequestBodyUserDTOdto){...}PutMapping(/profile)publicResultupdateProfile(Validated(UserDTO.UpdateGroup.class)RequestBodyUserDTOdto){...}PostMapping(/audit)publicResultaudit(Validated(UserDTO.AuditGroup.class)RequestBodyUserDTOdto){...}一个 DTO、三套规则、零if分支。字段规则变更只改一处新增场景只加一个分组接口。九、常见坑清单指定分组后忘了写 groups的约束全失效——Email没写groups用CreateGroup触发时它不执行。解决办法让分组extends Default或明确给约束标组。Spring 里用Valid传分组——Valid不支持分组参数指定分组必须用Validated(分组.class)。类上和方法参数上同时写Validated——方法参数级会覆盖类级避免混淆分组写在需要的那一层。嵌套对象分组没对齐——内层 DTO 的约束挂了内层分组外层触发的是外层分组用ConvertGroup显式映射。GroupSequence不生效——顺序组必须通过Default触发把顺序组作为Default的定义直接validate(dto, 某顺序组.class)不会按序列执行。链式 API 里找分组——链式 API 没有分组别在field(...)上找group()方法那是注解方式的特性。分组接口误加方法——分组接口必须是空接口纯标记加方法会破坏分组语义。十、最佳实践清单分组接口就近嵌套放在 DTO 内部UserDTO.CreateGroup字段分组一目了然公共规则上提父组多场景共用的约束挂父组子组extends父组避免重复标注必填差异用分组格式校验放 Default格式类约束Email、PhoneNumber、AlphaNum默认挂 Default 全场景生效只有必填与否的差异才用分组区分一个字段一个语义同一字段多分组标注时groups数组一次写全别用两行相同约束制造歧义Spring 中分组只写在方法参数Validated(CreateGroup.class)直接标注在方法参数上作用域最清晰顺序校验才用 GroupSequence普通创建/更新差异不需要顺序别为用而用文档写明分组触发点接口文档标注创建/更新/审核分别校验哪些分组让调用方知道错误信息的范围与链式 API 分工多场景 DTO 用注解 分组动态数据临时校验用链式 API各司其职。总结分组验证是 Bean Validation 标准机制ValidX 所有注解原生支持groups()与NotBlank/Size等标准注解完全混用0 配置核心三要素定义分组接口 → 注解标groups→ 验证时传分组不写groups的约束属于Default组指定非 Default 分组后它们会失效可用分组 extends Default合并组继承让公共规则只写一次GroupSequence提供顺序验证与短路Spring 中用Validated(分组.class)指定分组Valid不支持ValidX 的能力边界注解方式支持分组链式 APIchain/ValidX.java不支持——多场景 DTO 复用请走注解方式。ValidX 是基于 Jakarta Bean Validation 规范的 Java 验证库注解与链式 API 双模式内置 100 验证规则。项目地址github.com/vipxieliang/validx
返回列表