Springboot:更好地处理错误消息

Gre*_*een 9 java validation error-handling spring-boot

我正在使用 Spring Boot 开发一个 API,目前我正在考虑如何以一种易于国际化的方式处理错误消息。我的目标如下:

  1. 在资源文件/包中定义错误消息
  2. @Length以声明方式将约束注释与错误消息(例如,)连接起来
  3. 错误消息包含占位符,例如{min},它们被注释中的相应值替换(如果可用),例如,@Length(min = 5, message = msg)将导致类似 的结果msg.replace("{min}", annotation.min()).replace("{max}", annotation.max())
  4. JSON 属性路径也可用作占位符,并在发生验证错误时自动插入到错误消息中。
  5. 错误处理程序之外的解决方案是首选,即,当异常到达错误处理程序时,它们已经包含所需的错误消息。
  6. 来自资源包的错误消息会自动注册为 Java 中的常量。

目前,我自定义了methodArgumentNotValidHandler错误处理程序类的读取内容ObjectErrore.getBindingResult().getAllErrors()然后尝试提取它们的参数和错误代码,以决定从资源包中选择哪个错误消息并相应地对其进行格式化。我的代码的粗略草图如下所示:

输入:

@Data
@RequiredArgsConstructor
public class RequestBody {
  @NotNull
  @NotBlank(message = ErrorConstants.NOT_BLANK)
  @Length(min = 5, max = 255, message = ErrorConstants.LENGTH_MIN_MAX) // LENGTH_MIN_MAX = validation.length.min-max
  private String greeting;
}
Run Code Online (Sandbox Code Playgroud)

错误处理程序:

@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
ErrorMessage methodArgumentNotValidHandler(MethodArgumentNotValidException e) {
  ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
  Object[] arguments = objectError.getArguments();
  String messageCode = objectError.getDefaultMessage(); // e.g., "validation.length.min-max" (key in resource bundle)
  ResourceBundle errMsgBundle = ResourceBundle.getBundle("errorMsg");
  String message;
  if (objectError.getCode().equals("Length")) {
    String messageTemplate = errMsgBundle.getString(messageCode);
    message = String.format(messageTemplate, arguments[2], arguments[1]);
  } else {
    message = "Bad input, but I cannot tell you the problem because the programmer hasn't handled this yet. Sorry :'(";
  }
  return new ErrorMessage(message);
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,我认为这种方法不可维护。在错误处理程序中,我最终会得到一个巨大的 if-else 块,它必须探测几种不同的情况(错误代码、参数数量……)并相应地格式化错误消息。更改错误消息可能会导致必须更改代码(例如,参数的顺序)。每个属性键必须作为常量存在ErrorConstants,我认为这是不可取的。此代码也不查询错误属性的名称或路径,例如“name”。

因此,

  1. 有没有一种解决方案可以满足上述部分或全部要求?
  2. 我会在哪个地方实施这个?
  3. 对于上述问题至少还有更好的解决方案吗?
  4. SpringBoot 中是否有处理验证错误的方法或模式(我绝对不是第一个考虑这个问题的人)?

Gre*_*een 1

经过一番挖掘后,我发现我正在寻找的东西确实已经内置了,因为我希望每个想要很好地表现自己的开发人员都会问这个问题。事实上,这个问题已经被问过(如果我能正确表达我的要求,我本可以更早找到它)。我只是要求通过资源包自定义我的本地化错误消息。

当我在包含自定义错误消息的资源文件夹中创建资源包并将其命名为“validation_errors.properties”时,我可以通过创建相应的 bean 来使验证器使用这些消息:

@Bean
public Validator validatorFactory (MessageSource messageSource) {
    LocalValidatorFactoryBean validator =  new LocalValidatorFactoryBean();
    validator.setValidationMessageSource(messageSource);
    return validator;
}

@Bean
public MessageSource messageSource() {
    ReloadableResourceBundleMessageSource bean = new ReloadableResourceBundleMessageSource();
    bean.addBasenames("classpath:org.hibernate.validator.ValidationMessages", "classpath:validation_errors"); // validation_errors.properties is my resource bundle
    bean.setDefaultEncoding("UTF-8");
    return bean;
}
Run Code Online (Sandbox Code Playgroud)

我的自定义验证器从 的实例检索验证消息ReloadableResourceBundleMessageSource,该实例又从属性文件中检索它们。

属性文件包含验证注释的“消息”参数的限定路径作为键和值任意字符串,其中大括号中的字符串被验证注释中的参数替换,并且计算SpEL 表达式。

javax.validation.constraints.NotNull.message = Not null please!
javax.validation.constraints.NotBlank.message = Not empty please!
org.hibernate.validator.constraints.Length.message = String length between {min} and {max} please!
Run Code Online (Sandbox Code Playgroud)

接下来,在我的错误处理程序中,我需要检测并解包ObjectError中的实例是否MethodArgumentNotValidException包含 a ConstraintViolation(为了简化此示例,我忽略其他错误源):

@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
List<ErrorMessage> methodArgumentNotValidHandler(MethodArgumentNotValidException e) {
    return e.getBindingResult().getAllErrors().stream()
            .filter(objectError -> objectError.contains(ConstraintViolation.class))
            .map(objectError -> objectError.unwrap(ConstraintViolation.class))
            .map(ConstraintViolation::getMessage)
            .map(message -> new ErrorMessage("VE-400", message))
            .collect(Collectors.toList());
}
Run Code Online (Sandbox Code Playgroud)

该解决方案满足要求 1、3、5 和 6。要求 2 被视为无效,因为它与我提出此问题时想到的特定解决方案相关。要求 4 仍然开放,SpEL 可能有可能进一步研究,否则我将继续探索Tris 答案