找回密码
 立即注册
首页 业界区 安全 离谱!加了一个 @NotNull,接口竟然返回两条重复报错? ...

离谱!加了一个 @NotNull,接口竟然返回两条重复报错?

松菊 昨天 19:25
问题现象

有个项目新增了一个接口,这个接口的请求参数里面定义了一个字段,这个字段使用了 @NotNull 注解修饰,同时这个对象上使用了 Lombok 的 @Data 注解修饰。然后调用这个接口的时候提示信息有重复的。如下图所示:
1.png

问题复现

首先定义了一个 TestDTO,它的类上使用了 @Data 注解修饰,它的字段上使用 @NotNull 注解修饰。代码如下:
  1. @Data  
  2. public class TestDTO {  
  3.     @NotNull(message = "消息不能为空")  
  4.     private String message;  
  5. }
复制代码
然后是 HelloController,它的 test() 方法的参数使用了 @Valid 注解修饰。代码如下:
  1. @RestController  
  2. @Validated  
  3. public class TestController {  
  4.     @PostMapping("/test")  
  5.     public String test(@RequestBody @Valid TestDTO testDTO) {  
  6.         return "测试";  
  7.     }  
  8. }
复制代码
然后定义了全局的异常处理器,将 MethodArgumentNotValidException 异常中的的错误信息获取到生成 ApiResponse 并返回。代码如下:
  1. @RestControllerAdvice  
  2. public class GlobalAdvice {  
  3.     @ExceptionHandler(MethodArgumentNotValidException.class)  
  4.     public ApiResponse<?> handleException(MethodArgumentNotValidException ex) {  
  5.         List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();  
  6.         String defaultMessage = allErrors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(","));  
  7.         return ApiResponse.error(400, defaultMessage);  
  8.     }  
  9. }
复制代码
项目依赖的 lombok 版本是 1.18.24 ,如下图所示:
2.png

依赖的 Hibernate Validator 的版本是 6.0.22 ,如下图所示:
3.png

这个问题定位了很久没有找到原因,所以当时就在 GlobalAdvice 的 handleException() 做了一下去重处理。代码如下:
  1. @RestControllerAdvice  
  2. public class GlobalAdvice {  
  3.     @ExceptionHandler(MethodArgumentNotValidException.class)  
  4.     public ApiResponse<?> handleException(MethodArgumentNotValidException ex) {  
  5.                 // 这里做了一个去重处理
  6.         List<ObjectError> allErrors = ex.getBindingResult().getAllErrors().stream().distinct().collect(Collectors.toList());  
  7.         String defaultMessage = allErrors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(","));  
  8.         return ApiResponse.error(400, defaultMessage);  
  9.     }  
  10. }
复制代码
去重后接口返回的错误提示信息不重复了,如下图所示:
4.png

问题原因

Lombok 版本

首先是 lombok 的原因,在上面的代码中,虽然是在 TestDTO 的 message 字段上使用的 @NotNull 注解修饰的,但是 lombok 在生成它的 getter() 和 setter() 方法时,会把字段上的注解也复制到方法的参数上,这样在字段和方法参数上都有 @NotNull 注解修饰了。如下图所示:
5.png

在 lombok 的 HandlerUtil 里面定义了 BASE_COPYABLE_ANNOTATIONS 的一个名单,在这个名单里面的注解在生成 getter() 或者 setter() 会进行拷贝,在 lombok 的 1.18.24 版本是配置了 javax.validation.constraints.NotNull 的。如下图所示:
6.png

这个注解是2021年10月份加进去的,如下图所示:
7.png

在2022年5月份被移除了,如下图所示:
8.png

Hibernate Validator 版本

其次是 Hibernate Validator 的版本,在 Hibernate Validator 中是通过 ConstraintViolationImpl 对象来表示的校验错误信息。在 6.0.22 版本里面生这个信息是在 ConstraintViolationImpl 的 createConstraintViolation() 方法中实现的。代码如下:
  1. public Set<ConstraintViolation<T>> createConstraintViolations(ValueContext<?, ?> localContext,
  2.     ConstraintValidatorContextImpl constraintValidatorContext) {
  3.     return constraintValidatorContext.getConstraintViolationCreationContexts().stream()
  4.         .map( c -> createConstraintViolation( localContext, c, constraintValidatorContext.getConstraintDescriptor() ) )
  5.         .collect( Collectors.toSet() );
  6. }
  7. public ConstraintViolation<T> createConstraintViolation(ValueContext<?, ?> localContext, ConstraintViolationCreationContext constraintViolationCreationContext, ConstraintDescriptor<?> descriptor) {
  8.         String messageTemplate = constraintViolationCreationContext.getMessage();
  9.         String interpolatedMessage = interpolate(
  10.                 messageTemplate,
  11.                 localContext.getCurrentValidatedValue(),
  12.                 descriptor,
  13.                 constraintViolationCreationContext.getPath(),
  14.                 constraintViolationCreationContext.getMessageParameters(),
  15.                 constraintViolationCreationContext.getExpressionVariables()
  16.         );
  17.         // at this point we make a copy of the path to avoid side effects
  18.         Path path = PathImpl.createCopy( constraintViolationCreationContext.getPath() );
  19.         Object dynamicPayload = constraintViolationCreationContext.getDynamicPayload();
  20.         switch ( validationOperation ) {
  21.             case PARAMETER_VALIDATION:
  22.                 return ConstraintViolationImpl.forParameterValidation(
  23.                         messageTemplate,
  24.                         constraintViolationCreationContext.getMessageParameters(),
  25.                         constraintViolationCreationContext.getExpressionVariables(),
  26.                         interpolatedMessage,
  27.                         getRootBeanClass(),
  28.                         getRootBean(),
  29.                         localContext.getCurrentBean(),
  30.                         localContext.getCurrentValidatedValue(),
  31.                         path,
  32.                         descriptor,
  33.                         localContext.getElementType(),
  34.                         executableParameters,
  35.                         dynamicPayload
  36.                 );
  37.             case RETURN_VALUE_VALIDATION:
  38.                 return ConstraintViolationImpl.forReturnValueValidation(
  39.                         messageTemplate,
  40.                         constraintViolationCreationContext.getMessageParameters(),
  41.                         constraintViolationCreationContext.getExpressionVariables(),
  42.                         interpolatedMessage,
  43.                         getRootBeanClass(),
  44.                         getRootBean(),
  45.                         localContext.getCurrentBean(),
  46.                         localContext.getCurrentValidatedValue(),
  47.                         path,
  48.                         descriptor,
  49.                         localContext.getElementType(),
  50.                         executableReturnValue,
  51.                         dynamicPayload
  52.                 );
  53.             default:
  54.                 return ConstraintViolationImpl.forBeanValidation(
  55.                         messageTemplate,
  56.                         constraintViolationCreationContext.getMessageParameters(),
  57.                         constraintViolationCreationContext.getExpressionVariables(),
  58.                         interpolatedMessage,
  59.                         getRootBeanClass(),
  60.                         getRootBean(),
  61.                         localContext.getCurrentBean(),
  62.                         localContext.getCurrentValidatedValue(),
  63.                         path,
  64.                         descriptor,
  65.                         localContext.getElementType(),
  66.                         dynamicPayload
  67.                 );
  68.         }
  69.     }
复制代码
最终所有的校验结果都是放在 ValidationContext 中的 failingConstraintViolations 属性中,而它是一个 Set 类型,那就会根据对象的 hashCode 值是否是同一个对象。代码如下:
  1. public class ValidationContext<T> {
  2.     private final Set<ConstraintViolation<T>> failingConstraintViolations;
  3.    
  4.     public void addConstraintFailures(Set<ConstraintViolation<T>> failingConstraintViolations) {
  5.                 this.failingConstraintViolations.addAll( failingConstraintViolations );
  6.         }
  7. }
复制代码
而在 6.0.22 版本里,ConstraintViolationImpl 的 createHashCode() 方法是包含了 elementType 的,那么字段和 getter() 方法创建对象计算出来的 hashCode 是不一样的。代码如下:
  1. private int createHashCode() {
  2.     int result = interpolatedMessage != null ? interpolatedMessage.hashCode() : 0;
  3.     result = 31 * result + ( propertyPath != null ? propertyPath.hashCode() : 0 );
  4.     result = 31 * result + System.identityHashCode( rootBean );
  5.     result = 31 * result + System.identityHashCode( leafBeanInstance );
  6.     result = 31 * result + System.identityHashCode( value );
  7.     result = 31 * result + ( constraintDescriptor != null ? constraintDescriptor.hashCode() : 0 );
  8.     result = 31 * result + ( messageTemplate != null ? messageTemplate.hashCode() : 0 );
  9.     result = 31 * result + ( elementType != null ? elementType.hashCode() : 0 );
  10.     return result;
  11. }
复制代码
但是在 6.2.0.Final 版本里,ConstraintViolationImpl 的 createHashCode() 方法把 elementType 给移除了,那么字段和 getter() 方法创建对象计算出来的 hashCode 是不一样的,从而达到了去重的目的。如下图所示:
9.png

通过在 6.2.0.Final 版本实际调试后发现,字段和 getter() 方法生成的校验对象的 hashCode值是一样,这样在 ValidationContext 中的 failingConstraintViolations 属性中最终只会存放一个对象,接口的返回值也会只有一个,不会有重复的错误提示了。如下图所示:
10.png

11.png


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册