Hibernate-Validator扩展之自定义注解
一、Hibernate-Validator介绍
Hibernate-Validator框架提供了一系列的注解去校验字段是否符合预期,如@NotNull注解可以校验字段是否为null,如果为null则抛出对应的异常提示信息,通过注解大大减少了我们日常的开发工作量。包括流行的spring-boot-starter-validation,底层也是靠Hibernate-Validator实现的。
但是在实际的开发中,现有的注解可能不能满足我们的校验需求,Hibernate-Validator框架就贴心的提供了扩展,通过自定义校验注解来封装我们自己的校验逻辑。
二、自定义校验注解
下面以一个例子去说明如何根据自己的业务需求,去自定义校验注解。
需求背景:
在日常开发中,我们经常需要在Controller接口对入参的字段做校验,而且有些字段的值只允许在某个枚举定义范围内,如果不在枚举范围内,则抛出异常和错误信息。针对这种情况,我们可以自定义一个注解@StatusCodeCheck去实现。
根据官方文档的描述,自定义校验注解需要如下三个步骤:
- 创建一个约束注解
- 实现一个校验器
- 定义默认的错误信息
2.1创建一个约束注解
创建之前可以先看看@NotNull的源码
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(NotNull.List.class)
@Documented
@Constraint(
validatedBy = {}
)
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
NotNull[] value();
}
}
@Target: 该元注解用于指定注解的作用范围,包括类,方法,字段等。
@Retention: 指定该元注解的保留策略,包括source,class和runtime。
@Repeatable: 这是一个很有意思的元注解,在没有@Repeatable
注解的的注解中,在同一个地方使用相同的注解会报错,有了此元注解注解的注解,就可以在同一个地方使用相同的注解。
@Documented: 表示会生成Java doc
@Constraint: 用于指定校验器,通过校验器返回的结果(true/false)来判断是否抛出异常信息。
除了这些元注解,还有一些属性,其作用写在了注释中
package cn.sp.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* @author Ship
* @version 1.0.0
* @description:
* @date 2021/11/10 16:52
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = StatusCodeCheckValidator.class)
@Documented
public @interface StatusCodeCheck {
// 指定校验失败时的异常信息,后面会详细说明
String message() default "{cn.sp.validation.StatusCode.message}";
// 分组,如同一个实体类的字段有些情况需要该校验,有些情况不需要,则可通过指定分组实现
Class<?>[] groups() default {};
// 指定错误的级别,一般不会用
Class<? extends Payload>[] payload() default {};
// 自定义的属性
Class<? extends StatusCode> value();
}
StatusCode是一个泛型的接口,所以需要使用@StatusCodeCheck注解的枚举都需要实现StatusCode接口,主要起一个标记作用。
public interface StatusCode<T> {
/**
* 获取code
*
* @return
*/
T getCode();
}
2.2 实现一个校验器
实现一个校验器很简单,创建一个类实现ConstraintValidator接口即可,ConstraintValidator的源码如下
public interface ConstraintValidator<A extends Annotation, T> {
default void initialize(A constraintAnnotation) {
}
boolean isValid(T var1, ConstraintValidatorContext var2);
}
该接口是一个泛型接口,A表示作用的注解,T表示被校验对象的类型,里面有两个方法需要实现。
initialize(A constraintAnnotation)
校验器的初始化逻辑,一般用于获取自定义注解的属性,该方法是可选的。
isValid(T var1, ConstraintValidatorContext var2)
该方法有两个参数,var1为被校验的对象,var2是一个上下文提供了很多API去操作默认约束信息等,返回值表示校验是否通过,即真正的校验逻辑处理都在该方法中完成。
知道这些后,就可以开始写自己的校验器StatusCodeCheckValidator了
/**
* @author Ship
* @version 1.0.0
* @description:
* @date 2021/11/10 17:06
*/
public class StatusCodeCheckValidator implements ConstraintValidator<StatusCodeCheck, Object> {
private Class<? extends StatusCode> enumClass;
/**
* 枚举缓存
*/
private static final Map<Class<? extends StatusCode>, List<StatusCode>> CACHE_MAP = new ConcurrentHashMap<>(64);
@Override
public void initialize(StatusCodeCheck constraintAnnotation) {
this.enumClass = constraintAnnotation.value();
}
@Override
public boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {
if (object == null) {
return false;
}
if (!enumClass.isEnum()) {
throw new RuntimeException("StatusCode 的实现类必须是枚举类型");
}
List<StatusCode> statusCodeList = CACHE_MAP.computeIfAbsent(enumClass, (key) -> {
try {
Method method = key.getDeclaredMethod("values");
StatusCode[] statusCodes = (StatusCode[]) method.invoke(null);
return Stream.of(statusCodes).collect(Collectors.toList());
} catch (Exception e) {
e.printStackTrace();
}
return Lists.newArrayList();
});
for (StatusCode statusCode : statusCodeList) {
if (statusCode.getCode().equals(object)) {
return true;
}
}
return false;
}
}
这里通过反射来获取所有的枚举实例,后面发现用 EnumSet.of() 方法也是可以的,出于好奇就看了下它的源码,发现底层也是通过反射调用values方法+缓存来实现的,这就叫万变不离其宗吧。
2.3定义默认的错误信息
@StatusCodeCheck注解的message属性可以指定默认错误信息,既用可以写死字符串的方式如
String message() default "error message";
也可以通过${}符号去读取ValidationMessages.properties文件配置的信息
String message() default "{javax.validation.constraints.NotNull.message}";
ValidationMessages.properties
cn.sp.validation.StatusCode.message=can not find code in {value}.
这里的{value}会读取@StatusCodeCheck注解的value,功能还是挺强大的。
三、测试
首先,编写测试代码,创建用于测试的枚举类ThirdPartyPlatformEnum
/**
* @author Ship
* @version 1.0.0
* @description
* @date 2021/11/02 11:25
*/
public enum ThirdPartyPlatformEnum implements StatusCode<String> {
/**
* 拼多多
*/
PDD("PDD", "拼多多"),
/**
* 天猫
*/
TIAN_MALL("TIAN_MALL", "天猫"),
/**
* 有赞
*/
YOU_ZAN("YOU_ZAN", "有赞"),
/**
* 美团
*/
MEI_TUAN("MEI_TUAN", "美团");
private String code;
private String desc;
ThirdPartyPlatformEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
@Override
public String getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
测试实体类ValidationTest
public class ValidationTest {
@StatusCodeCheck(message = "无效的第三方平台类型", value = ThirdPartyPlatformEnum.class)
private String thirdPartyPlatform;
public String getThirdPartyPlatform() {
return thirdPartyPlatform;
}
public void setThirdPartyPlatform(String thirdPartyPlatform) {
this.thirdPartyPlatform = thirdPartyPlatform;
}
}
测试接口
@RequestMapping("/validation")
@RestController
public class ValidationTestTestController {
@PostMapping("/test")
public void test(@RequestBody @Validated ValidationTest validationTest) {
System.out.println("validation test");
}
}
然后启动项目,请求接口http://localhost:9001/validation/test,请求参数如下
{
"thirdPartyPlatform":"ali"
}
控制台日志显示校验未通过,因为"ali"不在ThirdPartyPlatformEnum的code范围内。
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public void cn.sp.validation.test.ValidationTestTestController.test(cn.sp.validation.ValidationTest): [Field error in object 'validationTest' on field 'thirdPartyPlatform': rejected value [ali]; codes [StatusCodeCheck.validationTest.thirdPartyPlatform,StatusCodeCheck.thirdPartyPlatform,StatusCodeCheck.java.lang.String,StatusCodeCheck]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validationTest.thirdPartyPlatform,thirdPartyPlatform]; arguments []; default message [thirdPartyPlatform],class cn.sp.validation.ThirdPartyPlatformEnum]; default message [无效的第三方平台类型]] ]
改为PDD再次请求
{
"thirdPartyPlatform":"PDD"
}
发现控制台打印出了validation test,说明校验通过。
四、总结
Hibernate-Validator框架如何实现可以自定义注解的原理还需要深入研究下,同时在阅读英文官方文档时,感觉自己的英语水平还是不够啊。本文代码已经上传到github,如果有兴趣可以自行下载。
参考:
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#preface
Hibernate-Validator扩展之自定义注解的更多相关文章
- Hibernate validator使用和自定义validator及整合Spring MVC
http://blog.csdn.net/lwphk/article/details/43983669 Hibernate validator使用 导入validation-api-xxx.jar 以 ...
- 用spring的@Validated注解和org.hibernate.validator.constraints.*的一些注解在后台完成数据校验
这个demo主要是让spring的@Validated注解和hibernate支持JSR数据校验的一些注解结合起来,完成数据校验.这个demo用的是springboot. 首先domain对象Foo的 ...
- hibernate validator参数校验&自定义校验注解
参数校验:简单的就逐个手动写代码校验,推荐用Valid,使用hibernate-validator提供的,如果参数不能通过校验,报400错误,请求格式不正确: 步骤1:在参数对象的属性上添加校验注解如 ...
- Hibernate Validator注解大全
hibernate Validator 是 Bean Validation 的参考实现 .Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现, ...
- SpringBoot Validation参数校验 详解自定义注解规则和分组校验
前言 Hibernate Validator 是 Bean Validation 的参考实现 .Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的 ...
- hibernate validator【原】
hibernate validator 功能 在开发中经常做一些字段校验的功能,比如非空,长度限制,邮箱验证等等,为了省掉这种冗长繁琐的操作,hibernate validator提供了一套精简的注释 ...
- SpringMVC--使用hibernate validator数据校验
JSR 303 Spring3开始支持JSR 303 验证框架,JSR303是Java为Bean数据合法性校验所提供的标准框架.JSR 303 支持XML和注解风格的验证,通过在Bean属性上标注类似 ...
- 使用Hibernate Validator来帮你做数据校验
数据校验是贯穿所有应用程序层(从表示层到持久层)的常见任务.通常在每个层中实现相同的验证逻辑,这是耗时且容易出错的.这里我们可以使用Hibernate Validator来帮助我处理这项任务.对此,H ...
- hibernate validator自定义校验注解以及基于服务(服务组)的校验
hibernate validator是Bean Validation 1.1 (JSR 349) Reference Implementation,其广泛的应用在mvc的参数校验中,尤其是使用服务端 ...
- hibernate validation内置注解及自定义注解
Bean Validation 中内置的 constraint @Null 被注释的元素必须为 null @NotNull 被注释的元素必须不为 null @AssertTrue 被注释的元素必须为 ...
随机推荐
- 【图论】CF1508C Complete the MST
Problem Link 有一张 \(n\) 个点的完全图,其中 \(m\) 条边已经标有边权.你需要给剩下的边都标上权值,使得所有边权的异或和为 \(0\),并且整张图的最小生成树边权和最小. \( ...
- 你不知道的<input type="file">的小秘密
限制file上传类型 很多时候,我们都需要使用 <input type="file"> 进行文件上传. 在上传的时候,我们需要对文件类型进行限制. 如果上传图片的时候. ...
- 【分享一个工具】根据 /metrics 路径下的文本信息,自动生成包含所有 metrics 的 grafana 报表
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 在做某个服务对应的 grafana 监控报表的时候发现,一 ...
- Loguru:Python 日志终极解决方案
关注微信公众号:K哥爬虫,持续分享爬虫进阶.JS/安卓逆向等技术干货! 日志的重要性 日志的作用非常重要,日志可以记录用户的操作.程序的异常,还可以为数据分析提供依据,日志的存在意义就是为了能够在程序 ...
- Qt连接MySQL的问题解决方法
Qt5在连接MySQL数据库时会出现一些问题,本文介绍两种最常见的问题,以及其相对简单的解决办法. Qt5数据库支持列表里没有MySQL 输入以下代码查看支持的数据库类型 //打印支持的数据库类型 q ...
- uni-app 实现下拉刷新功能
我们在运用uni-app开发小程序或h5时,常常需要页面实现下拉刷新功能. 在 js 中定义 onPullDownRefresh 处理函数(和onLoad等生命周期函数同级),监听该页面用户下拉刷新事 ...
- OpenIM (Open-Source Instant Messaging) Mac Deployment Guide
This guide provides step-by-step instructions for deploying OpenIM on a Mac, including both source c ...
- layui之静态表格的分页及搜索功能以及前端使用XLSX导出Excel功能
LayUI官方文档:https://layui.dev/docs/2/#introduce XLSX NPM地址:https://www.npmjs.com/package/xlsx XLSX 使用参 ...
- 【一】tensorflow【cpu/gpu、cuda、cudnn】全网最详细安装、常用python镜像源、tensorflow 深度学习强化学习教学
相关文章: [一]tensorflow安装.常用python镜像源.tensorflow 深度学习强化学习教学 [二]tensorflow调试报错.tensorflow 深度学习强化学习教学 [三]t ...
- scrapy抓取校花网图片
一:基础版(抓取首页图片) 爬虫py文件代码: 1 # -*- coding: utf-8 -*- 2 import scrapy 3 import sys 4 import io 5 from sc ...