关于 Hibernate Validator 的使用
之前学习过 hibernate Validator,但当时做的笔记弄丢了,最近可能又要开始写 Java,所以把这一部分学习一下,做个笔记。
Hibernate Validator 用于值校验,能够避免业务中过多地出现校验业务代码。Hibernate Validator 支持对自定义类型,集合类型和内置 Java 类型进行校验,同时支持定义校验组,即让类型约束从属于特定组,只在进行该组的校验时才发挥作用。
为什么要使用 Hibernate Validator:
- 声明式,避免到处写丑陋的样板代码
- 更清晰地规范实体定义,校验注解本身就是一种注释
- 支持自定义校验逻辑和错误消息,其中还能注入依赖以实现更复杂逻辑
环境搭建和基本使用
通过下面的 starter 引入 hibernate validator 依赖:
1 |
|
Hibernate Validator 利用切面完成自己的逻辑,它根据类上的@Validated
注解去进行切入,并根据参数上的注解去进行校验。总的来说,要让校验起效,需要:
spring-boot-starter-validation
依赖引入- 类上标注
@Validated
(**要且必须要在类上标注!必须是@Validated
而非@Valid
**) - 方法的参数上标注相应校验注解(其中,实体类参数使用
@Valid
或@Validated
注解) - 调用方法时不能从内部调用(切面嘛,懂的都懂)
注意,Hibernate Validator 通过切面工作,因此它不仅能切入 Controller,也能切入 Service,但仅此而已,某些时候还是需要手动进行校验。
下面是一个极简例子,涉及到控制器和实体类的校验,它已经提出了许多要注意的部分:
1 |
|
@Valid
校验能进行嵌套校验,但它会直接忽略掉 null,因此上面的SomeDto
定义中,next
字段为 null,以及dtos
字段为 null 或空集合,或集合中存在 null,都是容忍的。
Controller 的参数校验似乎并非完全是通过切面完成的——即使 Controller 上未加@Validated
注解,@Valid
注解仍旧会生效,但是其它校验注解不会生效,因此最佳实践是,总是在类上加@Validated
,不要嫌麻烦。
关于 Service 的校验
Hibernate Validator 有一条规则:A method overriding another method must not redefine the parameter constraint configuration
,它是说,子类无法覆盖掉父类上的校验注解,即使父类上没有校验注解。
上面的规则同时暗示了,Hibernate Validator 的注解是能够继承的。一般而言,Spring 项目中 Service 的接口和实现是分离的,如果要校验 Service,根据上面的规则,我们应当:
- 在接口上标注
@Validated
注解 - 在接口上的方法参数中添加相应校验注解
- 在实现上不需要添加任何注解,或者保证实现上的注解和接口上的完全相同。
初看感觉这个要求不太合理,但细想其实还好——按理来说,接口内部使用何种实现对接口的调用者是透明的,因此值的约束必须是定义自接口层级上的,实现对值的约束只能更宽,不能更窄,而我们无法判断约束的宽窄,所以就硬性要求它们保持一致。但其实作为业务的开发者来说,还是希望能够将注解只写在实现上。
常用校验注解
这里列出可能会常用的注解,注意几乎所有注解都认为 null 是合法的。注解主要在org.hibernate.validator.constraints
和javax.validation.constraints
包下。
注解 | 作用 | 坑 |
---|---|---|
检查邮箱是否合法 | null 合法 | |
Past, Future, … | 时间是否是过去或未来 | null 合法 |
Pattern | 字符串必须满足正则 | null 合法 |
Size | 字符串长度或集合必须满足特定大小范围 | null 合法,大小区间前闭后闭 |
Min, Max, Positive, Negative… | 限制数字的最小值,最大值,正负性等 | null 合法,注意不要用 Min 和 Max 限制字符串长度,这个能启动,但运行时会报错 |
Length | 字符串长度必须在特定范围 | null 合法,前闭后闭 |
Null | 约束字段必须为 null | |
NotNull | 约束字段必须不能为 null | |
NotEmpty | 集合或字符串不能为 null 且非空 | |
NotBlank | 字符串不能为 null 且必须包含非空字符 |
手动校验实体类
有时候可能会想要进行手动校验,比如我们可能会想写 mybatis 拦截器,在插入和更新数据前进行校验,手动校验可以利用 Spring 提供的 Beanjavax.validation.Validator
(这是它对 JSR 规范的实现),它返回”vioiations”,即实体对象对约束的违反。
1 |
|
拦截校验异常
Hibernate Validator 会抛出如下异常:
org.springframework.web.bind.MethodArgumentNotValidException
,抛出在 Controller 的@Valid
或@Validated
注解的实体类参数(这简直就是历史遗留问题),默认响应码是 400,消息是”Bad Request”javax.validation.ConstraintViolationException
,其他情况,默认响应码是 500,消息是”Internal Server Error”,这是符合道理的——控制器的参数错误是用户的错误,服务层的参数错误是开发者的错误。
这两个异常都需要被拦截才能妥善把校验信息响应给前端……但这样真的好吗?全给到前端不是会让坏家伙有可乘之机吗?总之贴上(实际操作时应当像 ruoyi 那样,正常响应和错误响应形式保持一致):
1 |
|
自定义校验注解
有时候需要自定义校验,比如要校验身份证,这个肯定没有被提供,需要手写。这时候需要自定义 Validator 和注解。
Validator 不关心它被标注在字段上还是标注到参数上,它直接拿到值然后去做校验,但同时也允许获取当前的字段路径等信息以构建错误消息。
Spring Boot 会惰性地为为每个不同的注解创建相应的 Validator 实例,从而让每个 Validator 都负责同一个注解(类型相同,且所有字段值相同),避免任何并发问题,同时支持在创建 Validator 时注入依赖。
编写自定义校验注解需要:
- 创建自定义注解,注解需要是 Runtime 的,需要能够标注到字段和参数上(也可以让注解能标注到类上,这允许对整个类进行校验),注解需要引用下一步中编写的自定义 Validator,注解必须包含 groups,message, payload 字段(可以直接抄现成的)
- 创建自定义 Validator,如果注解能够校验多种类型,则每个类型都需要一个 Validator,Validator 类要实现
ConstraintValidator<注解,T>
,在构造函数中注入 Spring Bean 依赖,在initialize
方法中注入注解,并在isValid
中线程安全地进行校验。 - 实现
isValid
时,第一个参数是字段值,第二个参数是当前上下文,isValid
方法返回 true 或 false,true 表示校验通过,false 表示不通过,此时 hibernate validator 会根据上下文去构造相应 violation,此时可以自定义错误消息。
下面编写一个身份证校验注解,其中演示了如何在校验过程中获取 Bean 使得能和系统其他部分进行交互,以及如何修改错误消息。
1 |
|
分组校验
所有校验注解都有groups
参数(除了 Validated,它直接用value
),表示校验所属的校验组。在进行校验时,通过@Validated
的 value 参数指定只校验特定组的注解(注意它标识在参数上时表示只校验这些组的注解,标识在字段上时表示该校验属于这些组,这是两种不同的语义)。校验组使用**任意 Interface **进行标识,这些 Inteface 不需要任何实际操作。
分组有如下性质:
- 未指定 groups 的校验注解,默认属于
javax.validation.groups.Default
组,因此一旦指定了校验组,那没有处在任何校验组中的校验注解不会生效。 - 校验组可以继承,表示它同时对应多个组,比如可以**定义组去继承
Default
**,这样即使在 groups 参数中只指定该组,也会校验到未指定 groups 的校验注解。 - 可以使用
GroupSequence
表示按顺序校验多个组,但它不会引入继承关系。
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!