如何在Controller Advice中获取自定义校验器传递的消息参数?
我有一个自定义校验器DescriptionLengthValidator,需要通过消息参数动态构建错误信息。该校验器通过HibernateConstraintValidatorContext添加了消息参数,但在Controller Advice的handleMethodArgumentNotValid方法中,调用FieldError.getArguments()无法获取这些参数。请问如何修改自定义校验器或Controller Advice以获取消息参数?
现有代码
自定义校验器代码
public class DescriptionLengthValidator implements ConstraintValidator<ValidTypeDescriptionLength, RequestDto> { @Override public boolean isValid(RequestDto request, ConstraintValidatorContext context) { int maxSize = request.getType().equals("Category1") ? 50 : 250; boolean isValid = request.getDescription().length() <= maxSize; if (!isValid) { HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class); hibernateContext.disableDefaultConstraintViolation(); hibernateContext.addMessageParameter("0", "0").addMessageParameter("1", maxSize) .buildConstraintViolationWithTemplate("error.Size").addPropertyNode("description") .addConstraintViolation(); return false; } return true; } }
注解定义代码
@Constraint(validatedBy = DescriptionLengthValidator.class) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ValidTypeDescriptionLength { String message() default "error.Size"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Controller Advice代码
@ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { Map<String, String> fieldErrors = ex.getBindingResult().getFieldErrors().stream().collect( Collectors.toMap(FieldError::getField, fe -> getFieldErrorMessage(fe, String.format("Invalid value [%s]", fe.getRejectedValue())))); return handleFieldValidationErrors(ex, status, request, fieldErrors); } private String getFieldErrorMessage(FieldError error, String defaultMessage) { Object[] args = error.getArguments(); if (args != null && args.length > 0 && args[0] instanceof MessageSourceResolvable) { args = Arrays.copyOfRange(args, 1, args.length); } List<String> errorCodes = new ArrayList<>(); if (error.getCodes() != null) { Arrays.stream(error.getCodes()).map(s -> "error." + s).forEach(errorCodes::add); } if (error.getDefaultMessage() != null) { errorCodes.add(error.getDefaultMessage()); } return service.getMessage(args, defaultMessage); } }
解决方案
方案一:修改自定义校验器,使用表达式变量传递参数
Hibernate Validator的addMessageParameter添加的参数不会自动映射到Spring的FieldError参数中,改用addExpressionVariable并结合EL表达式,Spring会将这些变量解析到FieldError的arguments里。
修改后的校验器代码:
public class DescriptionLengthValidator implements ConstraintValidator<ValidTypeDescriptionLength, RequestDto> { @Override public boolean isValid(RequestDto request, ConstraintValidatorContext context) { int maxSize = request.getType().equals("Category1") ? 50 : 250; int minSize = 0; boolean isValid = request.getDescription().length() <= maxSize; if (!isValid) { HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class); hibernateContext.disableDefaultConstraintViolation(); // 使用addExpressionVariable添加变量,对应message中的EL表达式 hibernateContext.addExpressionVariable("min", minSize) .addExpressionVariable("max", maxSize) // 模板中使用EL变量,或者直接在注解message中定义 .buildConstraintViolationWithTemplate("{min}到{max}") .addPropertyNode("description") .addConstraintViolation(); return false; } return true; } }
同时修改注解的默认message,确保包含EL变量:
@Constraint(validatedBy = DescriptionLengthValidator.class) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ValidTypeDescriptionLength { // 这里的message可以直接包含EL变量,或者指向消息配置文件中的key String message() default "描述长度必须在{min}到{max}之间"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
此时在Controller Advice的getFieldErrorMessage方法中,error.getArguments()就能获取到min和max对应的参数值。
方案二:从ConstraintViolation中提取自定义参数
如果不想修改校验器,可以在Controller Advice中直接从MethodArgumentNotValidException获取底层的ConstraintViolation集合,从中提取Hibernate添加的消息参数。
修改后的Controller Advice代码:
@ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { // 构建字段错误映射时,同时获取ConstraintViolation的参数 Map<String, String> fieldErrors = new HashMap<>(); // 获取所有ConstraintViolation Set<ConstraintViolation<?>> violations = ex.getBindingResult().getAllErrors().stream() .map(ObjectError::unwrap) .filter(ConstraintViolation.class::isInstance) .map(ConstraintViolation.class::cast) .collect(Collectors.toSet()); // 处理每个FieldError,关联对应的ConstraintViolation参数 ex.getBindingResult().getFieldErrors().forEach(fe -> { // 找到当前字段对应的ConstraintViolation Optional<ConstraintViolation<?>> violationOpt = violations.stream() .filter(v -> v.getPropertyPath().toString().equals(fe.getField())) .findFirst(); String errorMessage = getFieldErrorMessage(fe, violationOpt, String.format("Invalid value [%s]", fe.getRejectedValue())); fieldErrors.put(fe.getField(), errorMessage); }); return handleFieldValidationErrors(ex, status, request, fieldErrors); } private String getFieldErrorMessage(FieldError error, Optional<ConstraintViolation<?>> violationOpt, String defaultMessage) { Object[] args = error.getArguments(); if (args != null && args.length > 0 && args[0] instanceof MessageSourceResolvable) { args = Arrays.copyOfRange(args, 1, args.length); } // 如果有ConstraintViolation,提取其中的消息参数 if (violationOpt.isPresent()) { ConstraintViolation<?> violation = violationOpt.get(); // 获取Hibernate添加的消息参数 Map<String, Object> messageParameters = violation.getMessageParameters(); // 将参数转换为数组,顺序对应模板中的占位符 args = new Object[]{messageParameters.get("0"), messageParameters.get("1")}; } List<String> errorCodes = new ArrayList<>(); if (error.getCodes() != null) { Arrays.stream(error.getCodes()).map(s -> "error." + s).forEach(errorCodes::add); } if (error.getDefaultMessage() != null) { errorCodes.add(error.getDefaultMessage()); } return service.getMessage(args, defaultMessage); } }
这种方式不需要修改校验器,直接从底层的ConstraintViolation中获取getMessageParameters()就能拿到之前用addMessageParameter添加的参数。
内容的提问来源于stack exchange,提问作者Chamod Pathirana

