news 2026/8/9 10:23:08

离谱!加了一个 @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/8 8:04:54

基于springboot + vue师生交流论坛系统(源码+数据库+文档)

师生交流论坛 目录 基于springboot vue师生交流论坛系统 一、前言 二、系统功能演示 ​编辑 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue师生交流论坛系统 一、前言 博主介…

作者头像 李华
网站建设 2026/8/8 5:20:02

如何用AI加速Inkscape矢量图形设计

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个Inkscape插件概念&#xff0c;能够通过AI自动识别位图并转换为可编辑矢量路径。功能包括&#xff1a;智能描摹参数调节、自动识别色块区域、一键生成SVG代码。要求支持Kimi…

作者头像 李华
网站建设 2026/8/8 7:56:11

企业级Maven私服镜像搭建实战:Nexus3全攻略

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个详细的Nexus3私有Maven仓库搭建指南应用&#xff0c;包含以下内容&#xff1a;1) Nexus3安装部署步骤(Docker和原生安装两种方式)&#xff1b;2) 仓库类型配置详解(hosted/…

作者头像 李华
网站建设 2026/8/9 3:15:01

FXSound实战:5个专业音频处理场景应用案例

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个FXSound应用案例展示平台&#xff0c;包含5个典型使用场景&#xff1a;1. 播客人声清晰化处理&#xff1b;2. 游戏环境音效增强&#xff1b;3. 音乐母带处理&#xff1b;4.…

作者头像 李华
网站建设 2026/8/9 2:05:59

Vue新手必看:v-on事件处理错误的5个常见原因

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个面向Vue.js初学者的教学项目&#xff0c;解释error in v-on handler错误的常见原因。要求&#xff1a;1) 用最简单的代码示例展示5种典型错误场景&#xff1b;2) 对每种错误…

作者头像 李华