当前位置: 首页 > news >正文

离谱!加了一个 @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属性中最终只会存放一个对象,接口的返回值也会只有一个,不会有重复的错误提示了。如下图所示:

http://www.cnnetsun.cn/news/48267.html

相关文章:

  • DeepSeek-Coder-V2:开源代码大模型的性能突破与行业影响
  • 代码解读dc
  • 网络安全需掌握的专业术语解析
  • 基于springboot + vue学生管理系统(源码+数据库+文档)
  • RPCS3多实例并行运行终极指南:突破单进程限制的完整解决方案
  • 告别瞎忙!16K星开源神器自动追踪时间
  • Inkscape在教学中的10个创新应用场景
  • 11.7亿参数挑战270亿模型性能:LFM2-1.2B-Extract重塑边缘智能文档处理
  • 从SyntaxError看Python交互式环境与脚本执行的差异
  • SeaTunnel终极指南:企业级数据集成完整解决方案
  • 微服务零风险发布:pig框架全链路灰度部署终极指南
  • 基于vue的大学生课堂考勤系统设计与实现_y72yw292_springboot php python nodejs
  • 基于vue的宠物健康档案信息管理系统_vxut6u52_springboot php python nodejs
  • 基于vue的旅游公司汽车租赁系统_g80v5wl4_springboot php python nodejs
  • 用AI优化音频处理:FXSound的智能增强技术解析
  • 列车售票|基于Java+ vue列车售票系统(源码+数据库+文档)
  • 需求可测试性分析Checklist:构建高质量软件测试的基石
  • 告别繁琐确认:AI如何提升操作效率
  • 视频处理性能瓶颈突破:ffmpeg-python管道化流式架构实战指南
  • 30分钟用LVM搭建弹性云存储原型
  • 鼠标手势革命:3分钟解锁10倍操作效率的终极指南
  • 1小时验证创意:用SenseVoice搭建语音控制智能家居原型
  • 腾讯HunyuanVideo提示词重写模型:让普通用户也能生成专业级视频的终极指南
  • AI如何帮你一键生成Xshell替代方案
  • 阿里:扩散模型强化学习框架d-TreeRPO
  • 33、商业技术管理中的外包、供应商管理与预算策略
  • 腾讯开源混元3D-Omni:四模态控制重构3D资产生产流程,效率提升10倍
  • 如何用AI自动生成天气API调用代码?快马平台3步搞定
  • AI如何帮你封装完美的axios请求库?
  • 终极Dell笔记本风扇控制教程:开源工具完整配置指南