news 2026/6/15 11:47:38

离谱!加了一个 @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/6/11 23:49:43

基于Java+ vue垃圾分类系统(源码+数据库+文档)

垃圾分类 目录 基于springboot vue垃圾分类系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue垃圾分类系统 一、前言 博主介绍&#xff1a;✌️大…

作者头像 李华
网站建设 2026/6/15 11:49:59

基于springboot + vue出行旅游安排系统(源码+数据库+文档)

出行旅游安排 目录 基于springboot vue出行旅游安排系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue出行旅游安排系统 一、前言 博主介绍&…

作者头像 李华
网站建设 2026/6/15 7:25:23

重新定义Restreamer:从零开始的流媒体转发神器使用指南

重新定义Restreamer&#xff1a;从零开始的流媒体转发神器使用指南 【免费下载链接】restreamer The Restreamer is a complete streaming server solution for self-hosting. It has a visually appealing user interface and no ongoing license costs. Upload your live str…

作者头像 李华
网站建设 2026/6/15 2:02:41

AI助力打造个性化Batocera游戏整合包

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个能够自动生成Batocera游戏整合包的AI工具。该工具应具备以下功能&#xff1a;1. 扫描指定文件夹中的游戏ROM文件&#xff0c;自动识别游戏名称、平台和版本&#xff1b;2. …

作者头像 李华
网站建设 2026/6/15 15:41:09

如何用AI快速解决ORA-28040错误?

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个工具&#xff0c;能够自动分析ORA-28040错误日志&#xff0c;识别错误原因&#xff08;如认证协议不匹配、密码过期等&#xff09;&#xff0c;并生成相应的修复代码或SQL语…

作者头像 李华
网站建设 2026/6/15 19:35:16

如何用AI自动生成GitHub Token管理工具

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个GitHub Token管理工具&#xff0c;支持以下功能&#xff1a;1. 通过GitHub API自动生成个人访问Token&#xff1b;2. 提供Token权限配置界面&#xff0c;可勾选repo、admin…

作者头像 李华