Tratamento Global de Erros em Spring - Básico

Nesse post rápido vou lhe ensinar como criar um tratamento de erros global no spring boot. Para um artigo avançado sobre o tema veja esse post

Basicamente precisamos criar um ControllerAdvice que irá interceptar exceções de tipos especificos, ou todas as exceções se quiser capturar uma RuntimeException.

Crie o arquivo em sua camada de controladores (exemplo package controller)

@ControllerAdvice
public class GlobalExceptionHandler {
  /**
   * todo translate message codes
   * Handles all DomainException
   * @param ex the exception to handle
   * @return ErroMessage
   */
  @ExceptionHandler(DomainException.class)
  public ResponseEntity<ErrorResponse> handleAllDomainException(DomainException ex) {
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
      .body(new ErrorResponse(ex.getMessage()));
  }
}

Nesse caso estamos capturando um tipo de erro chamado DomainException, que é uma classe da aplicação (package domain):

public class DomainException extends RuntimeException {

  @Getter
  private final Message domainMessage;

  public DomainException(Message domainMessage) {
    super(domainMessage.toString());
    this.domainMessage = domainMessage;
  }
}

Nosso ErrorResponse é um objeto de resposta (DTO) que está na camada de controladores:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;

@Value
public class ErrorResponse {
  private final String message;
  @JsonCreator
  public ErrorResponse(@JsonProperty("message") String message) {
    this.message = message;
  }
}

Note que essa classe aqui é Imutável, pois não faz sentido modificar o valor de message após criada. Para isso usamos o @JsonCreator para dizer qual é o contrutor a ser usado pelo Jackson Mapper para serializar para json. Caso contrário ele falha sem um construtor padrão.

Nota: O tipo de retorno ResponseEntity só funcionará em aplicações REST. Para aplicações diferentes (MVC por exemplo). Pode-se retornar um ModelAndView que irá renderizar um template html por exemplo.

Você pode interceptar outros tipos de erro como por exemplo DataIntegrityViolationException que é lançada quando há uma constraint no banco que não foi respeitada (unique, not null, etc..). Ou mesmo interceptar todos os Errors

Abaixo segue um exemplo mais completo de ControlerAdvice:

@ControllerAdvice
    public class ExceptionHandlerConfig extends ResponseEntityExceptionHandler {

      private final Logger logger;
      private final PropertiesMessageProxy messageProxy;

      public ExceptionHandlerConfig(Logger logger, PropertiesMessageProxy messageProxy) {
        this.logger = logger;
        this.messageProxy = messageProxy;
      }

      /**
       * Validates all Runtime exceptions, transforming to ApiError with generic message
       *
       * @param e Exception
       * @return ApiError for Json consumption
       */
      @ResponseStatus(INTERNAL_SERVER_ERROR)
      @ExceptionHandler(Exception.class)
      @ResponseBody
      public ApiError handleAll(Exception e) {
        logger.error(e.getMessage(), e);
        return ApiError.fromMessage(INTERNAL_SERVER_ERROR,
            messageProxy.messageFromKey(MessageConstants.APPLICATION_EXCEPTION_GENERIC));
      }

      @ResponseStatus(UNPROCESSABLE_ENTITY)
      @ExceptionHandler(DomainBusinessException.class)
      @ResponseBody
      public ApiError handleBusinessException(DomainBusinessException e) {
        if (!e.getMessageConstants().isPresent()) {
          return ApiError.fromException(UNPROCESSABLE_ENTITY, e);
        } else {
          return ApiError.fromMessage(UNPROCESSABLE_ENTITY,
              messageProxy.messageFromKey(e.getMessageConstants().get()));
        }
      }

      /**
       * Handle @Valid exceptions in methods with @RequestBody
       *
       * @param ex MethodException
       * @param headers Default HttpHeaders
       * @param status Status of the request
       * @param request The web request
       * @return ApiError for Json consumption
       */
      @Override
      protected ResponseEntity<Object> handleMethodArgumentNotValid(
          MethodArgumentNotValidException ex,
          HttpHeaders headers,
          HttpStatus status,
          WebRequest request) {

        return ResponseEntity.badRequest().body(ApiError.fromMessage(HttpStatus.BAD_REQUEST,
            messageProxy.getMessageFromListErrors(ex.getBindingResult().getAllErrors())));
      }

      /**
       * @return ApiError for Json consumption
       */
      @ExceptionHandler({ConstraintViolationException.class})
      public ResponseEntity<ApiError> handleConstraintViolation(
          ConstraintViolationException ex) {
        List<String> errors = new ArrayList<>();
        for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
          errors.add(violation.getPropertyPath() + ": " + violation.getMessage());
        }

        ApiError apiError =
            ApiError.fromMessage(HttpStatus.BAD_REQUEST, errors);
        return ResponseEntity.badRequest().body(apiError);
      }
@Data // lombok annotation
    class ApiError implements Serializable {

      private Integer statusCode;
      private String error;
      private List<String> messages;

      private ApiError(Integer statusCode, String error, String message) {
        this(statusCode, error,
            Objects.nonNull(message) ? Collections.singletonList(message) : Collections.emptyList());
      }

      private ApiError(Integer statusCode, String error, List<String> messages) {
        this.statusCode = statusCode;
        this.error = error;
        this.messages = messages;
      }

      public ApiError() {
        this.messages = Collections.emptyList();
      }

      public static ApiError fromException(HttpStatus httpStatus, Exception exception) {
        return fromException(httpStatus, exception, httpStatus.getReasonPhrase());
      }

      public static ApiError fromException(HttpStatus httpStatus, Exception exception,
          String errorIdentifier) {
        return new ApiError(httpStatus.value(), errorIdentifier, exception.getMessage());
      }

      public static ApiError fromMessage(HttpStatus httpStatus, String message) {
        return new ApiError(httpStatus.value(), httpStatus.getReasonPhrase(), message);
      }

      public static ApiError fromMessage(HttpStatus httpStatus, List<String> message) {
        return new ApiError(httpStatus.value(), httpStatus.getReasonPhrase(), message);
      }

Não cobrimos:

  • Internacionalização e codigos de erro

  • Hierarquia bem definida de Exceptions

É isso espero que este post tenha sido útil.