问题现象
有个项目新增了一个接口,这个接口的请求参数里面定义了一个字段,这个字段使用了 @NotNull 注解修饰,同时这个对象上使用了 Lombok 的 @Data 注解修饰。然后调用这个接口的时候提示信息有重复的。如下图所示:
问题复现
首先定义了一个 TestDTO,它的类上使用了 @Data 注解修饰,它的字段上使用 @NotNull 注解修饰。代码如下:- @Data
- public class TestDTO {
- @NotNull(message = "消息不能为空")
- private String message;
- }
复制代码 然后是 HelloController,它的 test() 方法的参数使用了 @Valid 注解修饰。代码如下:- @RestController
- @Validated
- public class TestController {
- @PostMapping("/test")
- public String test(@RequestBody @Valid TestDTO testDTO) {
- return "测试";
- }
- }
复制代码 然后定义了全局的异常处理器,将 MethodArgumentNotValidException 异常中的的错误信息获取到生成 ApiResponse 并返回。代码如下:- @RestControllerAdvice
- public class GlobalAdvice {
- @ExceptionHandler(MethodArgumentNotValidException.class)
- public ApiResponse<?> handleException(MethodArgumentNotValidException ex) {
- List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
- String defaultMessage = allErrors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(","));
- return ApiResponse.error(400, defaultMessage);
- }
- }
复制代码 项目依赖的 lombok 版本是 1.18.24 ,如下图所示:
依赖的 Hibernate Validator 的版本是 6.0.22 ,如下图所示:
这个问题定位了很久没有找到原因,所以当时就在 GlobalAdvice 的 handleException() 做了一下去重处理。代码如下:- @RestControllerAdvice
- public class GlobalAdvice {
- @ExceptionHandler(MethodArgumentNotValidException.class)
- public ApiResponse<?> handleException(MethodArgumentNotValidException ex) {
- // 这里做了一个去重处理
- List<ObjectError> allErrors = ex.getBindingResult().getAllErrors().stream().distinct().collect(Collectors.toList());
- String defaultMessage = allErrors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(","));
- return ApiResponse.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() 方法中实现的。代码如下:- public Set<ConstraintViolation<T>> createConstraintViolations(ValueContext<?, ?> localContext,
- ConstraintValidatorContextImpl constraintValidatorContext) {
- return constraintValidatorContext.getConstraintViolationCreationContexts().stream()
- .map( c -> createConstraintViolation( localContext, c, constraintValidatorContext.getConstraintDescriptor() ) )
- .collect( Collectors.toSet() );
- }
- public ConstraintViolation<T> createConstraintViolation(ValueContext<?, ?> localContext, ConstraintViolationCreationContext constraintViolationCreationContext, ConstraintDescriptor<?> descriptor) {
- String messageTemplate = constraintViolationCreationContext.getMessage();
- String interpolatedMessage = interpolate(
- messageTemplate,
- localContext.getCurrentValidatedValue(),
- descriptor,
- constraintViolationCreationContext.getPath(),
- constraintViolationCreationContext.getMessageParameters(),
- constraintViolationCreationContext.getExpressionVariables()
- );
- // at this point we make a copy of the path to avoid side effects
- Path path = PathImpl.createCopy( constraintViolationCreationContext.getPath() );
- Object dynamicPayload = constraintViolationCreationContext.getDynamicPayload();
- switch ( validationOperation ) {
- case PARAMETER_VALIDATION:
- return ConstraintViolationImpl.forParameterValidation(
- messageTemplate,
- constraintViolationCreationContext.getMessageParameters(),
- constraintViolationCreationContext.getExpressionVariables(),
- interpolatedMessage,
- getRootBeanClass(),
- getRootBean(),
- localContext.getCurrentBean(),
- localContext.getCurrentValidatedValue(),
- path,
- descriptor,
- localContext.getElementType(),
- executableParameters,
- dynamicPayload
- );
- case RETURN_VALUE_VALIDATION:
- return ConstraintViolationImpl.forReturnValueValidation(
- messageTemplate,
- constraintViolationCreationContext.getMessageParameters(),
- constraintViolationCreationContext.getExpressionVariables(),
- interpolatedMessage,
- getRootBeanClass(),
- getRootBean(),
- localContext.getCurrentBean(),
- localContext.getCurrentValidatedValue(),
- path,
- descriptor,
- localContext.getElementType(),
- executableReturnValue,
- dynamicPayload
- );
- default:
- return ConstraintViolationImpl.forBeanValidation(
- messageTemplate,
- constraintViolationCreationContext.getMessageParameters(),
- constraintViolationCreationContext.getExpressionVariables(),
- interpolatedMessage,
- getRootBeanClass(),
- getRootBean(),
- localContext.getCurrentBean(),
- localContext.getCurrentValidatedValue(),
- path,
- descriptor,
- localContext.getElementType(),
- dynamicPayload
- );
- }
- }
复制代码 最终所有的校验结果都是放在 ValidationContext 中的 failingConstraintViolations 属性中,而它是一个 Set 类型,那就会根据对象的 hashCode 值是否是同一个对象。代码如下:- public class ValidationContext<T> {
- private final Set<ConstraintViolation<T>> failingConstraintViolations;
-
- public void addConstraintFailures(Set<ConstraintViolation<T>> failingConstraintViolations) {
- this.failingConstraintViolations.addAll( failingConstraintViolations );
- }
- }
复制代码 而在 6.0.22 版本里,ConstraintViolationImpl 的 createHashCode() 方法是包含了 elementType 的,那么字段和 getter() 方法创建对象计算出来的 hashCode 是不一样的。代码如下:- private int createHashCode() {
- int result = 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 );
- return result;
- }
复制代码 但是在 6.2.0.Final 版本里,ConstraintViolationImpl 的 createHashCode() 方法把 elementType 给移除了,那么字段和 getter() 方法创建对象计算出来的 hashCode 是不一样的,从而达到了去重的目的。如下图所示:
通过在 6.2.0.Final 版本实际调试后发现,字段和 getter() 方法生成的校验对象的 hashCode值是一样,这样在 ValidationContext 中的 failingConstraintViolations 属性中最终只会存放一个对象,接口的返回值也会只有一个,不会有重复的错误提示了。如下图所示:
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |