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
本文作者:烟味i
本文链接:https://www.cnblogs.com/2YSP/p/15546945.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端