news 2026/5/1 4:07:07

离谱!加了一个 @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/4/20 13:59:09

GLM-4.6技术深度解析:智能体系统与代码生成能力的重大突破

GLM-4.6技术深度解析&#xff1a;智能体系统与代码生成能力的重大突破 【免费下载链接】GLM-4.6 GLM-4.6在GLM-4.5基础上全面升级&#xff1a;200K超长上下文窗口支持复杂任务&#xff0c;代码性能大幅提升&#xff0c;前端页面生成更优。推理能力增强且支持工具调用&#xff0…

作者头像 李华
网站建设 2026/4/18 19:30:29

Markdown幻灯片制作终极指南:从入门到精通

Markdown幻灯片制作终极指南&#xff1a;从入门到精通 【免费下载链接】marp The site of classic Markdown presentation writer app 项目地址: https://gitcode.com/gh_mirrors/ma/marp 还在为制作精美幻灯片而烦恼吗&#xff1f;&#x1f914; 每次打开传统演示软件都…

作者头像 李华
网站建设 2026/4/17 22:34:35

AltStore:解锁iOS应用安装的全新体验

AltStore&#xff1a;解锁iOS应用安装的全新体验 【免费下载链接】AltStore AltStore is an alternative app store for non-jailbroken iOS devices. 项目地址: https://gitcode.com/gh_mirrors/al/AltStore 你是否曾经因为App Store的限制而无法安装某些实用应用&…

作者头像 李华
网站建设 2026/4/23 17:08:01

COLMAP三维重建终极指南:从零开始掌握多视图几何技术

COLMAP三维重建终极指南&#xff1a;从零开始掌握多视图几何技术 【免费下载链接】colmap COLMAP - Structure-from-Motion and Multi-View Stereo 项目地址: https://gitcode.com/GitHub_Trending/co/colmap COLMAP作为业界领先的三维重建工具&#xff0c;能够将普通照…

作者头像 李华
网站建设 2026/4/28 12:46:06

ThinkPHP开发效率提升300%的秘诀

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 生成一个完整的ThinkPHP企业官网CMS系统&#xff0c;包含&#xff1a;1.多语言支持 2.可视化页面构建器 3.SEO优化功能 4.表单收集系统 5.访客统计模块。要求使用最新的ThinkPHP 8.…

作者头像 李华
网站建设 2026/4/30 19:01:24

30分钟用os.path.splitext打造文件分析工具原型

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 快速开发一个文件分析工具原型&#xff0c;功能包括&#xff1a;1. 统计目录下各类扩展名的文件数量&#xff1b;2. 找出无扩展名文件&#xff1b;3. 识别重复扩展名。要求&#xf…

作者头像 李华