You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 05:58:12