Estoy desarrollando una API con Spring Boot y actualmente estoy pensando en cómo manejar los mensajes de error de una manera fácilmente internacionalizable. Mis objetivos son los siguientes:
@Length ) de forma declarativa{min} , que se reemplazan por el valor correspondiente de la anotación, si está disponible, por ejemplo, @Length(min = 5, message = msg) daría como resultado algo como msg.replace("{min}", annotation.min()).replace("{max}", annotation.max()) . Actualmente, personalicé el methodArgumentNotValidHandler de mi clase de controlador de errores para leer ObjectError s de e.getBindingResult().getAllErrors() y luego traté de extraer sus argumentos y códigos de error para decidir qué mensaje de error elegir de mi paquete de recursos y formatearlo en consecuencia. . Un boceto aproximado de mi código se ve de la siguiente manera:
Aporte:
@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; }Controlador de errores:
@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(); // eg, "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); } Desafortunadamente, supongo que este enfoque no se puede mantener. En el controlador de errores, terminaré con un gran bloque if-else que tiene que probar varias situaciones diferentes (códigos de error, cantidad de argumentos, ...) y formatear los mensajes de error en consecuencia. Cambiar los mensajes de error posiblemente resulte en tener que cambiar el código (por ejemplo, el orden de los argumentos). Cada clave de propiedad debe estar presente como una constante en ErrorConstants , lo que me parece indeseable. Este código tampoco consulta el nombre o la ruta de la propiedad defectuosa, por ejemplo, "nombre".
Por eso,
Puede ser que puedas tener
@ExceptionHandler(ConstraintViolationException.class) protected ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException e, WebRequest request){ return Optional.ofNullable(e).map(ConstraintViolationException::getConstraintViolations).map(this::createException).orElseGet(this::generateGenericError); }de la que puedes tener
private ErrorBody createException(FieldError fieldError) { return ErrorBody.builder() .code(fieldError.getCode()) .message(fieldError.getDefaultMessage()) .field(fieldError.getField()) .value(fieldError.getRejectedValue()) .build(); }Para que puedas usar
fieldError.getCode()
para mapear el valor clave del archivo de propiedades
No soy un gran admirador de las anotaciones javax.validation. Principalmente porque los objetos cuyas clases están anotadas con estos no se pueden probar fácilmente.
Lo que recomiendo es registrar una implementación org.springframework.validation.Validator en su clase de controlador anotado @RestController de la siguiente manera:
@InitBinder void initBinder(WebDataBinder binder) { if (binder.getTarget() == null) { return; } final var validator1 = // your validator1 instance //check if specific validator is eligible to validate request and its body if (validator1.supports(binget.getTarget().getClass()) { binder.setValidator(validator); } } Después de dicho registro, Spring invoca dicho validador para hacer coincidir la solicitud y su cuerpo y lanza MethodArgumentNotValidException si el validador rechazó cualquiera de los campos de objeto dados.
En su controlador de excepciones anotado con @ControllerAdvice (tenga en cuenta que su alcance es solo para solicitudes http) puede manejar dicha excepción de la siguiente manera:
@ExceptionHandler(MethodArgumentNotValidException.class) @ResponseBody ErrorMessage handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { final var errors = e.getAllErrors() return new ErrorMessage(/* populate your error message based on given errors */); }Si bien la implementación del validador podría haberse visto así:
@Override public void validate(Object target, Errors errors) { final var credentials = (Credentials) target; //reject username field with given c000 code if it's null if (isNull(credentials.getUsername())) { errors.rejectValue("username", "c000", "username cannot be null"); return; } if (credentials.getUsername().trim().isEmpty()) { errors.rejectValue("username", "c001", "username cannot be empty"); return; } if (credentials.getUsername().length() > 256) { errors.rejectValue("username", "c002", "username cannot be longer than 256 characters"); return; } }La ventaja de tal solución es:
Si tiene alguna pregunta o duda, solo pregunte.