java spring validation自定义校验如何比对两个字段呢?比如密码和确认密码字段必须相等
默认题主说的是类似 @NotNull
这种校验
可以通过自定义注解+自定义校验规则实现,但是为了这么简单的场景写那么多不太值得
多个字段之间的比较一般有业务属性,已经不是简单的“校验”了,做在校验里反而不灵活
另外确认密码这样的操作,前端校验即可,后端不需要校验
3 回答2.6k 阅读✓ 已解决
3 回答4.1k 阅读✓ 已解决
8 回答3.6k 阅读
4 回答2.8k 阅读✓ 已解决
2 回答2.6k 阅读✓ 已解决
3 回答2.5k 阅读✓ 已解决
3 回答1.7k 阅读✓ 已解决
说来真巧,最近写一个项目,也是要马上写到用户模块了,也要涉及密码一致校验问题,起初也想着前端做了验证,后端对于这点不做也行,就算做了,单独写在业务校验里确实也可以
不过后面越想越觉得不得劲,因为说起校验,除了咱们
@NotNull
,@Max
,@Digits
等在javax.validation
包中也就是Jakarta Bean Validation
规范(后续简称JBV
规范)中的校验,还有就是咱们所说的业务校验了,比如查看用户是否存在这种可能跟其它业务或者数据库打交道的,但是自己写也是可以不用@NotNull
,@Max
等注解,直接在业务校验中校验非空和最大值等,这看个人代码风格习惯了那我个人习惯肯定是尽量分开了,相当于
javax.validation
包中的属于简单校验那这么一说来,这个密码一致校验是属于什么校验呢?我当然认为他是简单校验了。
既然是简单校验那就没有必要写在后续的复杂校验逻辑中,虽然其实实现起来肯定也是简单两三行代码就解决了,但是我还是渴望着类似做到利用
JBV
规范的机制提供简单自定义注解达到最终校验效果,这样的复用性肯定是最好的一般咱们肯定要引
spring-boot-starter-validation
,而其JBV
规范的实现其实是Hibernate Validator
,所以我跑去看了一下其文档,关于自定义的地方(我用的Hibernate Validator
版本是6.2.0.Final哈)在Custom Constraints也就是自定义约束的时候,这里是支持Class-level constraints也就是类级别的约束,看翻译吧,主要就是处理对象中多个属性的关联校验
而类级别的自定义约束,实现也很简单,写一个检查一致性的注解
@CheckConsistency
放在需要被校验的类UserRequest
上这个注解
@CheckConsistency
定义的时候要被JBV
规范中的@Constraint
注解注释,并且提供一个怎么校验的校验器填充在其validatedBy
中,比如而这个自定义校验器
CheckConsistencyValidator
需要实现JBV
规范中的接口ConstraintValidator<CheckConsistency, UserRequest>
,其中的两个泛型分别就是自定义的注解@CheckConsistency
以及被校验的对象UserRequest
了基本实现都完成了,简单测试一下是可以的
V2版本
但是稍加注意,就会发现,现在的实现其实只是针对了
UserRequest
这一个被校验对象,假设还有个OrderRequest
对象,里面也有两个属性name
和nameAgain
要一致性校验,岂不是这上面的一套注解+
Validator
就失效了因此
@CheckConsistency
和ConstraintValidator
需要第二版,也就是升级为@CheckConsistencyV2
和ConstraintValidatorV2
结合刚才的问题,抽象出共同的,再把不同的作为配置配起来即可,那我们
@CheckConsistencyV2
应该变为可以看到注解中提供两个属性名字
fieldName1()
和fieldName2()
来配置这次需要一次性校验的字段名而
ConstraintValidatorV2
实现的接口ConstraintValidator
的泛型就不能再是UserRequest
了,而应该是Object
了,代码如下最后咱们使用也还是和之前一样,这次我们
UserRequest
也升级为UserRequestV2
。以示区别再来测试一把,一切还是ok的哈
到这里了,就如刚才所说,假如有个
OrderRequest
对象,里面也有两个属性name
和nameAgain
要一致性校验,那只是改改属性值,也很简单就复用了V3版本
看似一切都差不多了,但是我想到这时,我很不满意,为啥,因为我最讨厌的东西就在上面的实现中,也就是
fieldName1 = "password", fieldName2 = "passwordAgain"
中的"password"
和"passwordAgain"
这两个字符串很简单,假设某天
UserRequestV2
的password
属性名改为pw
,但是@CheckConsistencyV2
中的fieldName1 = "password"
却忘了改,而且打包也都不会报编译错误,可能最终造成现网问题(测试漏测的前提下哈),别问,问就是每次发现这种问题,都要查看代码提交记录,满嘴飘香了,真的是,吃"别人这种屎"也吃太多回了(这也是为啥我后面专门要整一个小工具AutoConstants,扯远了哈)也就是这种编译都不不会发现的错误,因此我决定再想起他办法,把
@CheckConsistencyV2
和CheckConsistencyValidatorV2
升级到@CheckConsistencyV3
,CheckConsistencyValidatorV3
来到V3,这次主要解决的问题就是声明哪些字段需要一致性校验时最好不要出现字符串这种以后可能出"雷"的做法,那怎样方法最好呢?当然是用注解啦,注解直接标在对应的字段上即可,这样以后改啥名字都无所谓了。
所以我们需要一个标记的注解
@ConsistencyGroup
来标注哪些字段需要做一致性校验当然我们的
@CheckConsistencyV3
就不需要什么fieldName1()
和fieldName2()
这些方法了,空的就行此时
UserRequestV3
就需要刚才新加的@ConsistencyGroup
注解做标注了最后,重要的实现方式是在
CheckConsistencyValidatorV3
中,处理也很简单,就是扫出有@ConsistencyGroup
注解的Field
,然后再做取值对比还是简单测试一下,很ok
V4版本
写到这里,我本以为我会很满意,结果可达鸭突然眉头一皱,觉得还是不行,为啥呢?因为假设被校验对象中有两组需要分别校验一致性的字段时,比如下面的
UserRequestTemp
其中
password
和passwordAgain
,name
和nameAgain
分别做一致性校验,此时我们V3版本就不管用了,这个问题我其实没有在V2中提到,但是V2是可以很简单就解决的,利用@Repeatable
注解即可,再新增一个@CheckConsistencyV2List
注解作为@CheckConsistencyV2
注解的复数容器从而
UserRequestTemp
就变为最后简单改改
CheckConsistencyValidatorV2
即可,我这里就不再赘述,回到我们V3的版本,我们是不能简单使用@Repeatable
做扩展的,因为我们的@ConsistencyGroup
注解只是一个标记注解,里面没有任何属性的此时其实我们可以直接想到的有两种办法
再增加一个
@ConsistencyGroup2
的类似@ConsistencyGroup
的分组注解-> 这样可以临时解决问题,但肯定不是可扩展的长期之道,毕竟增加一个分组注解,就需要修
改对应的
Validator
,这是不太符合开闭原则的@ConsistencyGroup
中新增一个方法,String key()
,这样不同分组通过key
的方式做区分-> 但是这种使用
String
来控制代码逻辑,又是一个不可控的雷点,前面我也提到了,当然也可以改成枚举
XXXEnum key()
,这样以后需要一组新的分组,去添加一个新的枚举即可,只是我还是很不喜欢,毕竟还是不太满足开闭原则
因此我理想的模式其实是可以新增东西,但是不要修改以前已经写好的任何逻辑,真正做到开闭原则,做到可扩展
为了达到可扩展的效果,我们以
UserRequestV4
举例理想状态下
password
和passwordAgain
一致性了,我新增一个@PasswordConsistency
注解即可达到一致性验证的效果name
和nameAgain
一致性了,我新增一个@NameConsistency
注解即可达到一致性验证的效果且上面实现的效果,是不需要我修改以前任何逻辑,只需要新增分组注解即可,即
好,为了实现上面效果,我们就必须引入元注解,也就是注解的注解才可以达到效果
那显然
@PasswordConsistency
和@NameConsistency
都是和之前的@ConsistencyGroup
的空注解,唯一不同的是它们都被新增的一个元注解@ConsistencyTag
标记(注意其@Target
此时是ElementType.ANNOTATION_TYPE
)这样
@ConsistencyTag
相当于是注解的抽象,类似实现类和接口的关系,@ConsistencyTag
这里就是一个"接口",我们的CheckConsistencyValidatorV4
只需要去关心处理@ConsistencyTag
这个"接口",不用关心它的实现了(也就是@PasswordConsistency
和@NameConsistency
),从而实现了可扩展,实现了开闭原则那我们来看关键的类
CheckConsistencyValidatorV4
,如下:可以看到,实现稍微复杂了一点点,但是思路都是刚才我讲的,那是不变的,还是来测试一下,检查一下效果
最后看到报错的描述是不同,就是相对之前,需要小小修改一下分组注解,以便可以区分报错,也就是每个分组注解都要加一个
message
方法,来指明报错以及这个方法的关键字名字定义在
@ConsistencyTag
中小结
好了,写到这,其实呢,基本已经满足了题主问题的答案了,并且相关思路和代码都罗列在上面(若是代码不是很清楚的,可以到github上查看)
但是嗷...说实在话,我还不想去睡的,因为都写到元注解这里了,我们可以说都已经在
Hibernate Validator
的基础上再搭建了一层关于一致性校验的元注解,也可说是一种规范,涉及的业务是对象多属性校验,那都到这地步了,为何不更近一步,去实现一个更加通用的对象多属性校验的元注解,不仅仅是针对一致性校验,还有很多其他校验,类似如也就是
age
超过24时,companyName
属性不能空,类似这种多属性的校验,提供一种能力,方便这种模式的扩展,当然为了这么做是希望这一套注解@ConditionNotNullSource
和@ConditionNotNullTarget
能复用,正因为是多属性,由于业务不同,不同属性还可以组合,所以这种复用情况确实很少见的,但是这种造轮子对于编程技能还是有些帮助的,V1到V4看似版本迭代,但是实际V1,V2,V3都是造轮子给别人使用,但是V4其实是两个任务
@ConsistencyTag
,@CheckConsistencyV4
和CheckConsistencyValidatorV4
)@PasswordConsistency
和@NameConsistency
)当然V4只是比较局限的给造轮子的人造轮子,想要写成更通用的达到V5,还是需要更多精力
但今晚因为时间关系,狗命要紧( ╯▽╰),还是早点休息吧,以后有机会再说了,当然题主你也可以自己试试咯~那就拜拜了~