news 2026/8/31 14:19:46

离谱!加了一个 @NotNull,接口竟然返回两条重复报错?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
离谱!加了一个 @NotNull,接口竟然返回两条重复报错?

问题现象

有个项目新增了一个接口,这个接口的请求参数里面定义了一个字段,这个字段使用了@NotNull注解修饰,同时这个对象上使用了 Lombok 的@Data注解修饰。然后调用这个接口的时候提示信息有重复的。如下图所示:

问题复现

首先定义了一个TestDTO,它的类上使用了@Data注解修饰,它的字段上使用@NotNull注解修饰。代码如下:

@DatapublicclassTestDTO{@NotNull(message="消息不能为空")privateStringmessage;}

然后是HelloController,它的test()方法的参数使用了@Valid注解修饰。代码如下:

@RestController@ValidatedpublicclassTestController{@PostMapping("/test")publicStringtest(@RequestBody@ValidTestDTOtestDTO){return"测试";}}

然后定义了全局的异常处理器,将MethodArgumentNotValidException异常中的的错误信息获取到生成ApiResponse并返回。代码如下:

@RestControllerAdvicepublicclassGlobalAdvice{@ExceptionHandler(MethodArgumentNotValidException.class)publicApiResponse<?>handleException(MethodArgumentNotValidExceptionex){List<ObjectError>allErrors=ex.getBindingResult().getAllErrors();StringdefaultMessage=allErrors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(","));returnApiResponse.error(400,defaultMessage);}}

项目依赖的 lombok 版本是1.18.24,如下图所示:

依赖的 Hibernate Validator 的版本是6.0.22,如下图所示:

这个问题定位了很久没有找到原因,所以当时就在GlobalAdvicehandleException()做了一下去重处理。代码如下:

@RestControllerAdvicepublicclassGlobalAdvice{@ExceptionHandler(MethodArgumentNotValidException.class)publicApiResponse<?>handleException(MethodArgumentNotValidExceptionex){// 这里做了一个去重处理List<ObjectError>allErrors=ex.getBindingResult().getAllErrors().stream().distinct().collect(Collectors.toList());StringdefaultMessage=allErrors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(","));returnApiResponse.error(400,defaultMessage);}}

去重后接口返回的错误提示信息不重复了,如下图所示:

问题原因

Lombok 版本

首先是 lombok 的原因,在上面的代码中,虽然是在TestDTOmessage字段上使用的@NotNull注解修饰的,但是 lombok 在生成它的getter()setter()方法时,会把字段上的注解也复制到方法的参数上,这样在字段和方法参数上都有@NotNull注解修饰了。如下图所示:

在 lombok 的HandlerUtil里面定义了BASE_COPYABLE_ANNOTATIONS的一个名单,在这个名单里面的注解在生成getter()或者setter()会进行拷贝,在 lombok 的1.18.24版本是配置了javax.validation.constraints.NotNull的。如下图所示:

这个注解是2021年10月份加进去的,如下图所示:

在2022年5月份被移除了,如下图所示:

Hibernate Validator 版本

其次是 Hibernate Validator 的版本,在 Hibernate Validator 中是通过ConstraintViolationImpl对象来表示的校验错误信息。在6.0.22版本里面生这个信息是在ConstraintViolationImplcreateConstraintViolation()方法中实现的。代码如下:

publicSet<ConstraintViolation<T>>createConstraintViolations(ValueContext<?,?>localContext,ConstraintValidatorContextImplconstraintValidatorContext){returnconstraintValidatorContext.getConstraintViolationCreationContexts().stream().map(c->createConstraintViolation(localContext,c,constraintValidatorContext.getConstraintDescriptor())).collect(Collectors.toSet());}publicConstraintViolation<T>createConstraintViolation(ValueContext<?,?>localContext,ConstraintViolationCreationContextconstraintViolationCreationContext,ConstraintDescriptor<?>descriptor){StringmessageTemplate=constraintViolationCreationContext.getMessage();StringinterpolatedMessage=interpolate(messageTemplate,localContext.getCurrentValidatedValue(),descriptor,constraintViolationCreationContext.getPath(),constraintViolationCreationContext.getMessageParameters(),constraintViolationCreationContext.getExpressionVariables());// at this point we make a copy of the path to avoid side effectsPathpath=PathImpl.createCopy(constraintViolationCreationContext.getPath());ObjectdynamicPayload=constraintViolationCreationContext.getDynamicPayload();switch(validationOperation){casePARAMETER_VALIDATION:returnConstraintViolationImpl.forParameterValidation(messageTemplate,constraintViolationCreationContext.getMessageParameters(),constraintViolationCreationContext.getExpressionVariables(),interpolatedMessage,getRootBeanClass(),getRootBean(),localContext.getCurrentBean(),localContext.getCurrentValidatedValue(),path,descriptor,localContext.getElementType(),executableParameters,dynamicPayload);caseRETURN_VALUE_VALIDATION:returnConstraintViolationImpl.forReturnValueValidation(messageTemplate,constraintViolationCreationContext.getMessageParameters(),constraintViolationCreationContext.getExpressionVariables(),interpolatedMessage,getRootBeanClass(),getRootBean(),localContext.getCurrentBean(),localContext.getCurrentValidatedValue(),path,descriptor,localContext.getElementType(),executableReturnValue,dynamicPayload);default:returnConstraintViolationImpl.forBeanValidation(messageTemplate,constraintViolationCreationContext.getMessageParameters(),constraintViolationCreationContext.getExpressionVariables(),interpolatedMessage,getRootBeanClass(),getRootBean(),localContext.getCurrentBean(),localContext.getCurrentValidatedValue(),path,descriptor,localContext.getElementType(),dynamicPayload);}}

最终所有的校验结果都是放在ValidationContext中的failingConstraintViolations属性中,而它是一个Set类型,那就会根据对象的 hashCode 值是否是同一个对象。代码如下:

publicclassValidationContext<T>{privatefinalSet<ConstraintViolation<T>>failingConstraintViolations;publicvoidaddConstraintFailures(Set<ConstraintViolation<T>>failingConstraintViolations){this.failingConstraintViolations.addAll(failingConstraintViolations);}}

而在6.0.22版本里,ConstraintViolationImplcreateHashCode()方法是包含了elementType的,那么字段和getter()方法创建对象计算出来的 hashCode 是不一样的。代码如下:

privateintcreateHashCode(){intresult=interpolatedMessage!=null?interpolatedMessage.hashCode():0;result=31*result+(propertyPath!=null?propertyPath.hashCode():0);result=31*result+System.identityHashCode(rootBean);result=31*result+System.identityHashCode(leafBeanInstance);result=31*result+System.identityHashCode(value);result=31*result+(constraintDescriptor!=null?constraintDescriptor.hashCode():0);result=31*result+(messageTemplate!=null?messageTemplate.hashCode():0);result=31*result+(elementType!=null?elementType.hashCode():0);returnresult;}

但是在6.2.0.Final版本里,ConstraintViolationImplcreateHashCode()方法把elementType给移除了,那么字段和getter()方法创建对象计算出来的 hashCode 是不一样的,从而达到了去重的目的。如下图所示:

通过在6.2.0.Final版本实际调试后发现,字段和getter()方法生成的校验对象的 hashCode值是一样,这样在ValidationContext中的failingConstraintViolations属性中最终只会存放一个对象,接口的返回值也会只有一个,不会有重复的错误提示了。如下图所示:

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 15:06:49

DeepSeek-Coder-V2:开源代码大模型的性能突破与行业影响

导语 【免费下载链接】DeepSeek-Coder-V2-Base 开源代码智能利器DeepSeek-Coder-V2&#xff0c;性能比肩GPT4-Turbo&#xff0c;支持338种编程语言&#xff0c;128K代码上下文&#xff0c;助力编程如虎添翼。 项目地址: https://ai.gitcode.com/hf_mirrors/deepseek-ai/DeepS…

作者头像 李华
网站建设 2026/8/31 3:25:05

代码解读dc

整个程序是一个基于进化算法的多模态融合架构搜索框架&#xff08;DC-NAS&#xff09;&#xff0c;核心目标是自动搜索最优的多模态特征融合架构&#xff0c;用于分类任务。以下是程序的完整执行流程&#xff0c;并同步说明各辅助文件的调用时机和作用&#xff1a; 一、初始化阶…

作者头像 李华
网站建设 2026/8/31 14:22:19

网络安全需掌握的专业术语解析

在网络安全领域&#xff0c;各种英文缩写构成了专业交流的基础语言。对于从业者和学习者而言&#xff0c;准确理解这些术语的含义和关联至关重要。本文将系统梳理网络安全的核心术语&#xff0c;帮助读者建立清晰的认知框架。一、基础设施与网络服务术语DNS - 域名系统&#xf…

作者头像 李华
网站建设 2026/8/31 6:10:17

基于springboot + vue学生管理系统(源码+数据库+文档)

学生管理 目录 基于springboot vue学生管理系统 一、前言 二、系统功能演示 详细视频演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue学生管理系统 一、前言 博主介绍&am…

作者头像 李华
网站建设 2026/8/30 3:49:20

RPCS3多实例并行运行终极指南:突破单进程限制的完整解决方案

RPCS3多实例并行运行终极指南&#xff1a;突破单进程限制的完整解决方案 【免费下载链接】rpcs3 PS3 emulator/debugger 项目地址: https://gitcode.com/GitHub_Trending/rp/rpcs3 你是否曾幻想过同时畅玩多个PS3经典游戏&#xff1f;当《神秘海域》的冒险与《最终幻想X…

作者头像 李华
网站建设 2026/8/31 6:40:42

告别瞎忙!16K星开源神器自动追踪时间

还在为不知道自己每一天都瞎忙了什么而烦恼吗&#xff1f;很多人试过手动记日志、打卡软件&#xff0c;要么因为隐私、或者安全性、繁琐坚持不了。这几天在Github上发现一款16K star的超级实用的工具 - ActivityWatch&#xff0c;它可以自动、无感地追踪你在设备上的所有活动&a…

作者头像 李华