2026/4/6 10:51:34
网站建设
项目流程
长春市网站推广,wordpress 5.0编辑器,建设小说网站小说源,网站在线优化问题现象
有个项目新增了一个接口#xff0c;这个接口的请求参数里面定义了一个字段#xff0c;这个字段使用了 NotNull 注解修饰#xff0c;同时这个对象上使用了 Lombok 的 Data 注解修饰。然后调用这个接口的时候提示信息有重复的。如下图所示#xff1a;问题复现
首先定…问题现象有个项目新增了一个接口这个接口的请求参数里面定义了一个字段这个字段使用了NotNull注解修饰同时这个对象上使用了 Lombok 的Data注解修饰。然后调用这个接口的时候提示信息有重复的。如下图所示问题复现首先定义了一个TestDTO它的类上使用了Data注解修饰它的字段上使用NotNull注解修饰。代码如下DatapublicclassTestDTO{NotNull(message消息不能为空)privateStringmessage;}然后是HelloController它的test()方法的参数使用了Valid注解修饰。代码如下RestControllerValidatedpublicclassTestController{PostMapping(/test)publicStringtest(RequestBodyValidTestDTOtestDTO){return测试;}}然后定义了全局的异常处理器将MethodArgumentNotValidException异常中的的错误信息获取到生成ApiResponse并返回。代码如下RestControllerAdvicepublicclassGlobalAdvice{ExceptionHandler(MethodArgumentNotValidException.class)publicApiResponse?handleException(MethodArgumentNotValidExceptionex){ListObjectErrorallErrorsex.getBindingResult().getAllErrors();StringdefaultMessageallErrors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(,));returnApiResponse.error(400,defaultMessage);}}项目依赖的 lombok 版本是1.18.24如下图所示依赖的 Hibernate Validator 的版本是6.0.22如下图所示这个问题定位了很久没有找到原因所以当时就在GlobalAdvice的handleException()做了一下去重处理。代码如下RestControllerAdvicepublicclassGlobalAdvice{ExceptionHandler(MethodArgumentNotValidException.class)publicApiResponse?handleException(MethodArgumentNotValidExceptionex){// 这里做了一个去重处理ListObjectErrorallErrorsex.getBindingResult().getAllErrors().stream().distinct().collect(Collectors.toList());StringdefaultMessageallErrors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(,));returnApiResponse.error(400,defaultMessage);}}去重后接口返回的错误提示信息不重复了如下图所示问题原因Lombok 版本首先是 lombok 的原因在上面的代码中虽然是在TestDTO的message字段上使用的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版本里面生这个信息是在ConstraintViolationImpl的createConstraintViolation()方法中实现的。代码如下publicSetConstraintViolationTcreateConstraintViolations(ValueContext?,?localContext,ConstraintValidatorContextImplconstraintValidatorContext){returnconstraintValidatorContext.getConstraintViolationCreationContexts().stream().map(c-createConstraintViolation(localContext,c,constraintValidatorContext.getConstraintDescriptor())).collect(Collectors.toSet());}publicConstraintViolationTcreateConstraintViolation(ValueContext?,?localContext,ConstraintViolationCreationContextconstraintViolationCreationContext,ConstraintDescriptor?descriptor){StringmessageTemplateconstraintViolationCreationContext.getMessage();StringinterpolatedMessageinterpolate(messageTemplate,localContext.getCurrentValidatedValue(),descriptor,constraintViolationCreationContext.getPath(),constraintViolationCreationContext.getMessageParameters(),constraintViolationCreationContext.getExpressionVariables());// at this point we make a copy of the path to avoid side effectsPathpathPathImpl.createCopy(constraintViolationCreationContext.getPath());ObjectdynamicPayloadconstraintViolationCreationContext.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 值是否是同一个对象。代码如下publicclassValidationContextT{privatefinalSetConstraintViolationTfailingConstraintViolations;publicvoidaddConstraintFailures(SetConstraintViolationTfailingConstraintViolations){this.failingConstraintViolations.addAll(failingConstraintViolations);}}而在6.0.22版本里ConstraintViolationImpl的createHashCode()方法是包含了elementType的那么字段和getter()方法创建对象计算出来的 hashCode 是不一样的。代码如下privateintcreateHashCode(){intresultinterpolatedMessage!null?interpolatedMessage.hashCode():0;result31*result(propertyPath!null?propertyPath.hashCode():0);result31*resultSystem.identityHashCode(rootBean);result31*resultSystem.identityHashCode(leafBeanInstance);result31*resultSystem.identityHashCode(value);result31*result(constraintDescriptor!null?constraintDescriptor.hashCode():0);result31*result(messageTemplate!null?messageTemplate.hashCode():0);result31*result(elementType!null?elementType.hashCode():0);returnresult;}但是在6.2.0.Final版本里ConstraintViolationImpl的createHashCode()方法把elementType给移除了那么字段和getter()方法创建对象计算出来的 hashCode 是不一样的从而达到了去重的目的。如下图所示通过在6.2.0.Final版本实际调试后发现字段和getter()方法生成的校验对象的 hashCode值是一样这样在ValidationContext中的failingConstraintViolations属性中最终只会存放一个对象接口的返回值也会只有一个不会有重复的错误提示了。如下图所示