Guia para manipulação de erros da API REST do Spring Boot
Publicados: 2022-03-11Manipular erros corretamente nas APIs e fornecer mensagens de erro significativas é um recurso muito desejável, pois pode ajudar o cliente da API a responder adequadamente aos problemas. O comportamento padrão tende a retornar rastreamentos de pilha difíceis de entender e, em última análise, inúteis para o cliente da API. Particionar as informações de erro em campos também permite que o cliente da API a analise e forneça melhores mensagens de erro ao usuário. Neste artigo, abordaremos como fazer o tratamento adequado de erros ao criar uma API REST com o Spring Boot.
Construir APIs REST com Spring tornou-se a abordagem padrão para desenvolvedores Java nos últimos dois anos. O uso do Spring Boot ajuda substancialmente, pois remove muito código clichê e permite a configuração automática de vários componentes. Vamos supor que você esteja familiarizado com os fundamentos do desenvolvimento de API com essas tecnologias antes de aplicar o conhecimento descrito aqui. Se você ainda não tem certeza sobre como desenvolver uma API REST básica, então você deve começar com este artigo sobre Spring MVC ou outro sobre como construir um Spring REST Service.
Tornando as respostas de erro mais claras
Ao longo deste artigo, usaremos o código-fonte hospedado no GitHub de um aplicativo que implementa uma API REST para recuperar objetos que representam pássaros. Ele tem os recursos descritos neste artigo e mais alguns exemplos de cenários de tratamento de erros. Veja um resumo dos endpoints implementados nesse aplicativo:
GET /birds/{birdId} | Obtém informações sobre um pássaro e lança uma exceção se não for encontrada. |
GET /birds/noexception/{birdId} | Essa chamada também obtém informações sobre um pássaro, exceto que não lança uma exceção caso o pássaro não seja encontrado. | POST /birds | Cria um pássaro. | </tr>
O módulo MVC do framework Spring vem com alguns ótimos recursos para ajudar no tratamento de erros. Mas cabe ao desenvolvedor usar esses recursos para tratar as exceções e retornar respostas significativas ao cliente da API.
Vejamos um exemplo da resposta padrão do Spring Boot quando emitimos um HTTP POST para o endpoint /birds
com o seguinte objeto JSON, que tem a string “aaa” no campo “mass”, que deve estar esperando um inteiro:
{ "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }
A resposta padrão do Spring Boot, sem o tratamento de erros adequado:
{ "timestamp": 1500597044204, "status": 400, "error": "Bad Request", "exception": "org.springframework.http.converter.HttpMessageNotReadableException", "message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@cba7ebc; line: 4, column: 17]", "path": "/birds" }
Bem… a mensagem de resposta tem alguns campos bons, mas é muito focada no que foi a exceção. A propósito, esta é a classe DefaultErrorAttributes
do Spring Boot. O campo timestamp
é um número inteiro que nem mesmo carrega informações de qual unidade de medida o timestamp está. O campo de exception
só é interessante para desenvolvedores Java e a mensagem deixa o consumidor da API perdido em todos os detalhes de implementação que são irrelevantes para eles . E se houvesse mais detalhes que pudéssemos extrair da exceção que originou o erro? Então, vamos aprender como tratar essas exceções corretamente e envolvê-las em uma representação JSON melhor para facilitar a vida de nossos clientes de API.
Como usaremos classes de data e hora do Java 8, primeiro precisamos adicionar uma dependência Maven para os conversores Jackson JSR310. Eles cuidam da conversão de classes de data e hora do Java 8 para representação JSON usando a anotação @JsonFormat
:
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>
Ok, então vamos definir uma classe para representar erros de API. Estaremos criando uma classe chamada ApiError
que possui campos suficientes para armazenar informações relevantes sobre erros que ocorrem durante as chamadas REST.
class ApiError { private HttpStatus status; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") private LocalDateTime timestamp; private String message; private String debugMessage; private List<ApiSubError> subErrors; private ApiError() { timestamp = LocalDateTime.now(); } ApiError(HttpStatus status) { this(); this.status = status; } ApiError(HttpStatus status, Throwable ex) { this(); this.status = status; this.message = "Unexpected error"; this.debugMessage = ex.getLocalizedMessage(); } ApiError(HttpStatus status, String message, Throwable ex) { this(); this.status = status; this.message = message; this.debugMessage = ex.getLocalizedMessage(); } }
A propriedade
status
mantém o status da chamada da operação. Será qualquer coisa de 4xx para sinalizar erros do cliente ou 5xx para significar erros do servidor. Um cenário comum é um código http 400 que significa um BAD_REQUEST, quando o cliente, por exemplo, envia um campo formatado incorretamente, como um endereço de email inválido.A propriedade
timestamp
contém a instância de data e hora de quando o erro ocorreu.A propriedade
message
contém uma mensagem amigável sobre o erro.A propriedade
debugMessage
contém uma mensagem do sistema que descreve o erro com mais detalhes.A propriedade
subErrors
contém um array de sub-erros que aconteceram. Isso é usado para representar vários erros em uma única chamada. Um exemplo seria erros de validação em que vários campos falharam na validação. A classeApiSubError
é usada para encapsulá-los.
abstract class ApiSubError { } @Data @EqualsAndHashCode(callSuper = false) @AllArgsConstructor class ApiValidationError extends ApiSubError { private String object; private String field; private Object rejectedValue; private String message; ApiValidationError(String object, String message) { this.object = object; this.message = message; } }
Então o ApiValidationError
é uma classe que estende o ApiSubError
e expressa os problemas de validação encontrados durante a chamada REST.
Abaixo, você verá alguns exemplos de respostas JSON que estão sendo geradas após implementarmos as melhorias descritas aqui, apenas para ter uma ideia do que teremos ao final deste artigo.
Aqui está um exemplo de JSON retornado quando uma entidade não é encontrada ao chamar o endpoint GET /birds/2
:
{ "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } }
Aqui está outro exemplo de JSON retornado ao emitir uma chamada POST /birds
com um valor inválido para a massa do pássaro:
{ "apierror": { "status": "BAD_REQUEST", "timestamp": "18-07-2017 06:49:25", "message": "Validation errors", "subErrors": [ { "object": "bird", "field": "mass", "rejectedValue": 999999, "message": "must be less or equal to 104000" } ] } }
Tratamento de erros de inicialização do Spring
Vamos explorar algumas das anotações do Spring que serão usadas para lidar com exceções.

RestController
é a anotação base para classes que manipulam operações REST.
ExceptionHandler
é uma anotação do Spring que fornece um mecanismo para tratar exceções que são lançadas durante a execução de manipuladores (operações do Controlador). Esta anotação, se usada em métodos de classes de controladores, servirá como ponto de entrada para tratar exceções lançadas somente dentro deste controlador. Ao todo, a maneira mais comum é usar @ExceptionHandler
em métodos das classes @ControllerAdvice
para que o tratamento de exceção seja aplicado globalmente ou a um subconjunto de controladores.
ControllerAdvice
é uma anotação introduzida no Spring 3.2 e, como o nome sugere, é “Advice” para vários controladores. Ele é usado para permitir que um único ExceptionHandler
seja aplicado a vários controladores. Desta forma podemos definir em um só lugar como tratar tal exceção e este handler será chamado quando a exceção for lançada de classes que são cobertas por este ControllerAdvice
. O subconjunto de controladores afetados pode ser definido usando os seguintes seletores em @ControllerAdvice
: annotations()
, basePackageClasses()
e basePackages()
. Se nenhum seletor for fornecido, o ControllerAdvice
será aplicado globalmente a todos os controladores.
Portanto, usando @ExceptionHandler
e @ControllerAdvice
, poderemos definir um ponto central para tratar exceções e envolvê-las em um objeto ApiError
com melhor organização do que o mecanismo padrão de tratamento de erros Spring Boot.
Tratamento de exceções
O próximo passo é criar a classe que tratará as exceções. Para simplificar, estamos chamando-o de RestExceptionHandler
e deve se estender do ResponseEntityExceptionHandler
do Spring Boot. Estaremos estendendo o ResponseEntityExceptionHandler
, pois ele já fornece algum tratamento básico de exceções do Spring MVC, portanto, adicionaremos manipuladores para novas exceções enquanto aprimoramos as existentes.
Substituindo exceções tratadas em ResponseEntityExceptionHandler
Se você der uma olhada no código-fonte de ResponseEntityExceptionHandler
, verá muitos métodos chamados handle******()
como handleHttpMessageNotReadable()
ou handleHttpMessageNotWritable()
. Vamos primeiro ver como podemos estender handleHttpMessageNotReadable()
para lidar com exceções HttpMessageNotReadableException
. Nós apenas temos que substituir o método handleHttpMessageNotReadable()
em nossa classe RestExceptionHandler
:
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "Malformed JSON request"; return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex)); } private ResponseEntity<Object> buildResponseEntity(ApiError apiError) { return new ResponseEntity<>(apiError, apiError.getStatus()); } //other exception handlers below }
Declaramos que no caso de uma HttpMessageNotReadableException
ser lançada, a mensagem de erro será “Solicitação JSON malformada” e o erro será encapsulado dentro do objeto ApiError
. Abaixo podemos ver a resposta de uma chamada REST com este novo método substituído:
{ "apierror": { "status": "BAD_REQUEST", "timestamp": "21-07-2017 03:53:39", "message": "Malformed JSON request", "debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]" } }
Como lidar com exceções personalizadas
Agora veremos como criar um método que trata uma exceção que ainda não foi declarada dentro do ResponseEntityExceptionHandler
do Spring Boot.
Um cenário comum para um aplicativo Spring que lida com chamadas de banco de dados é ter uma chamada para localizar um registro por seu ID usando uma classe de repositório. Mas se examinarmos o método CrudRepository.findOne()
, veremos que ele retorna null
se um objeto não for encontrado. Isso significa que, se nosso serviço apenas chamar esse método e retornar diretamente ao controlador, obteremos um código HTTP 200 (OK), mesmo que o recurso não seja encontrado. Na verdade, a abordagem adequada é retornar um código HTTP 404 (NÃO ENCONTRADO) conforme especificado na especificação HTTP/1.1.
Para lidar com esse caso, criaremos uma exceção personalizada chamada EntityNotFoundException
. Esta é uma exceção criada de forma personalizada e diferente de javax.persistence.EntityNotFoundException
, pois fornece alguns construtores que facilitam a criação do objeto, podendo-se optar por tratar a exceção javax.persistence
de forma diferente.
Dito isso, vamos criar um ExceptionHandler
para essa EntityNotFoundException
recém-criada em nossa classe RestExceptionHandler
. Para fazer isso, crie um método chamado handleEntityNotFound()
e anote-o com @ExceptionHandler
, passando o objeto de classe EntityNotFoundException.class
para ele. Isso sinaliza ao Spring que toda vez que EntityNotFoundException
é lançada, o Spring deve chamar esse método para lidar com isso. Ao anotar um método com @ExceptionHandler
, ele aceitará uma ampla variedade de parâmetros auto-injetados como WebRequest
, Locale
e outros conforme descrito aqui. Apenas forneceremos a própria exceção EntityNotFoundException
como um parâmetro para este método handleEntityNotFound
.
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { //other exception handlers @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity<Object> handleEntityNotFound( EntityNotFoundException ex) { ApiError apiError = new ApiError(NOT_FOUND); apiError.setMessage(ex.getMessage()); return buildResponseEntity(apiError); } }
Excelente! No método handleEntityNotFound()
, estamos definindo o código de status HTTP para NOT_FOUND
e usando a nova mensagem de exceção. Aqui está a resposta para o endpoint GET /birds/2
agora:
{ "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }
Conclusão
É importante ter o controle do tratamento de exceções para que possamos mapear adequadamente essas exceções para o objeto ApiError
e fornecer informações importantes que permitam aos clientes da API saber o que aconteceu. A próxima etapa a partir daqui seria criar mais métodos de manipulador (aqueles com @ExceptionHandler) para exceções lançadas no código do aplicativo. Há mais exemplos para algumas outras exceções comuns como MethodArgumentTypeMismatchException
, ConstraintViolationException
e outras no código do GitHub.
Aqui estão alguns recursos adicionais que ajudaram na composição deste artigo:
Baeldung - Tratamento de erros para REST com Spring
Spring Blog - Tratamento de exceções no Spring MVC