Como tratar exceções em uma API Java com @ControllerAdvice

Neste artigo você vai ver:

Nesse artigo, vamos revisar o que são e como criar exceções personalizadas, além de elaborar um mecanismo de tratativa e manipulação delas utilizando a anotação @ControllerAdvice em uma classe de forma centralizada em um projeto API Java com Spring Boot

Aqui, faremos a tratativa de dois tipos de erros: um relacionado a argumentos inválidos que chegam nas requisições e outro para quando acontece algum erro durante a execução de uma pesquisa no banco de dados.

O que são exceções?

Exceções são anomalias que acontecem durante processamentos da nossa aplicação. São eventos que não deveriam acontecer, mas acontecem, e por isso devemos saber como lidar com eles e impedir que nossa aplicação quebre.

Com isso, vamos falar sobre mecanismo de tratativa de exceções! Quando o implementamos, nós capturamos o erro, tratamos ele da forma mais efetiva possível e retornamos uma mensagem coerente para quem estiver consumindo os recursos da nossa aplicação.

Tratamento de exceções personalizadas com Java

O Java possui várias exceções internas, como ClassNotFoundException.class, NullPointerException.class, IOException.class etc. 

Entretanto, em muitos cenários, elas não conseguem descrever totalmente uma situação ou não são adequadas como retorno para pessoas usuárias. Consequentemente, nós, como devs, temos a autonomia para criar exceções personalizadas.

Além de criar essas exceções, também podemos gerenciá-las para que elas (e também outras do Java, Spring Framework etc.) sejam tratadas e retornem uma mensagem coerente para o cenário.

O episódio #11 do Zupcast te ajuda a entender melhor sobre alguns mitos e verdades sobre uma das linguagens mais utilizadas do mundo: o Java.

Temos algumas formas que poderíamos implementar essa tratativa de erros, mas neste artigo focaremos em uma maneira onde todos os tratamentos estarão centralizados em uma classe, oferecendo assim uma melhor visibilidade do que está sendo implementado e também maior facilidade de manutenção. 

Para isso, você basicamente vai precisar de três pontos importantes:

  1. classe responsável pelo gerenciamento global das exceções utilizando a anotação @ControllerAdvice;
  2. se necessário, classes para representar exceções internas;
  3. (opcional) classe para padronizar e representar qual deve ser o formato do erro retornado como resposta da API.

Como funciona a anotação @ControllerAdvice

Para a implementação da classe que gerencia todas as exceções utilizaremos a anotação @ControllerAdvice. Essa anotação é do Spring Framework e é utilizada para lidar com exceções lançadas em qualquer lugar da sua aplicação, não só pelo controller.

De acordo com a própria documentação do Spring, o @ControllerAdvice é uma especialização da anotação @Component, que permite manipular exceções em todo o aplicativo em um componente global.

Imagine o @ControllerAdvice como uma barreira que intercepta todas as exceções que acontecem dentro de um método/classe anotado com @RequestMapping antes de chegar na pessoa usuária final? Poderíamos visualizar da seguinte maneira:

Na imagem, temos duas caixas representando controllers: UserController e MovieController. Cada um desses conceitos está lançando uma exceção diferente (no UserController, o UserNotFoundException, e no MovieController, o MethodArgumentNotValidException), sendo representada por uma seta com saída de cada caixa para um barramento chamado “@ControllerAdvice CustomExceptionHandler”. Esse barramento é responsável por filtrar essas exceções e tratá-las para retornar às pessoas usuárias. Por fim, há uma seta saindo do barramento para uma caixa final nomeada “user”, representando o retorno da exceção tratada.
Diagrama que mostra @ControllerAdvice como uma barreira entre o que os controllers lançam de exceção e o que a pessoa usuária recebe como resposta da nossa API.

Vamos à implementação de todos esses conceitos?

O exemplo prático disso tudo será dividido em três passos:

  1. definição da classe ApiErrorMessage que representará como nossos erros chegarão às pessoas usuárias;
  2. implementação da classe CustomExceptionHandler para lidar inicialmente com erros de argumentos não válidos recebidos pelo UserController;
  3. implementação de uma exceção personalizada chamada UserNotFoundException para lidar quando não for encontrado uma pessoa usuária no banco de dados e adição da trativa no CustomExceptionHandler.

O exemplo que veremos aqui pode ser acessado pelo repositório arquitetura-hexagonal e tem a seguinte estrutura:

├── arquitetura-hexagonal
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── monicaribeiro
│   │   │           └── arquiteturahexagonal
│   │   │               ├── adapter
│   │   │               │   ├── inbound
│   │   │               │   │   └── controller
│   │   │               │   │       ├── request
│   │   │               │   │       │   └── CreateUserRequest.java
│   │   │               │   │       ├── response
│   │   │               │   │       │   ├── MovieResponse.java
│   │   │               │   │       │   └── UserResponse.java
│   │   │               │   │       └── UserController.java
│   │   │               │   └── outbound
│   │   │               │       ├── integration
│   │   │               │       │   ├── FindMovieAdapter.java
│   │   │               │       │   ├── OmdbClient.java
│   │   │               │       │   └── OmdbMovieResponse.java
│   │   │               │       └── repository
│   │   │               │           ├── GetUserByIdAdapter.java
│   │   │               │           ├── SaveUserAdapter.java
│   │   │               │           ├── UserEntity.java
│   │   │               │           └── UserRepository.java
│   │   │               ├── ArquiteturaHexagonalApplication.java
│   │   │               ├── config
│   │   │               │   ├── BeanConfig.java
│   │   │               │   └── exception
│   │   │               │       ├── ApiErrorMessage.java
│   │   │               │       ├── CustomExceptionHandler.java
│   │   │               │       └── UserNotFoundException.java
│   │   │               └── domain
│   │   │                   ├── domain
│   │   │                   │   ├── Movie.java
│   │   │                   │   └── User.java
│   │   │                   ├── ports
│   │   │                   │   ├── inbound
│   │   │                   │   │   ├── CreateUserUseCasePort.java
│   │   │                   │   │   └── GetUserByIdUseCasePort.java
│   │   │                   │   └── outbound
│   │   │                   │       ├── FindMovieAdapterPort.java
│   │   │                   │       ├── GetUserByIdAdapterPort.java
│   │   │                   │       └── SaveUserAdapterPort.java
│   │   │                   └── usecase
│   │   │                       ├── CreateUserUseCase.java
│   │   │                       └── GetUserByIdUseCase.java

Passo 1: definição da classe ApiErrorMessage 

Essa classe pode ser personalizada de acordo com as necessidades do projeto, mas nesse caso, que é educacional, retornaremos como resposta para users o status da requisição e uma lista com as descrições de erros que ocorreram. 

Ficaria como o exemplo abaixo:

com/monicaribeiro/arquiteturahexagonal/config/exception/ApiErrorMessage.java
package com.monicaribeiro.arquiteturahexagonal.config.exception;

import org.springframework.http.HttpStatus;

import java.util.Arrays;
import java.util.List;

public class ApiErrorMessage {

    private HttpStatus status;
    private List<String> errors;

    public ApiErrorMessage(HttpStatus status, List<String> errors) {
        super();
        this.status = status;
        this.errors = errors;
    }

    public ApiErrorMessage(HttpStatus status, String error) {
        super();
        this.status = status;
        errors = Arrays.asList(error);
    }
    //Getters and Setters
}

Passo 2: definição da classe CustomExceptionHandler para lidar com erros de argumentos inválidos recebidos pelo UserController 

É uma prática comum criar um tratamento personalizado para retornar mensagens de erro informativas e detalhadas quando users não enviam o payload conforme as regras estabelecidas pela API

No exemplo da aplicação que citamos, temos no controller um método para criação de users e ele aguarda a seguinte requisição:

com/monicaribeiro/arquiteturahexagonal/adapter/inbound/controller/request/CreateUserRequest.java
package com.monicaribeiro.arquiteturahexagonal.adapter.inbound.controller.request;

import com.monicaribeiro.arquiteturahexagonal.domain.domain.Movie;
import com.monicaribeiro.arquiteturahexagonal.domain.domain.User;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

public class CreateUserRequest {
    @NotNull(message = "Name is mandatory.")
    private String name;

    @NotBlank(message = "Favorite movie name is mandatory.")
    private String favoriteMovieTitle;

    //Getters and Setters
}

Nela, temos duas validações: uma para não aceitar valores nulos no parâmetro name e outra para não aceitar valores em branco no favoriteMovieTitle. Ambas possuem mensagens personalizadas que queremos retornar para a pessoa usuária e, para retorná-las, vamos criar nosso CustomExceptionHandler:

com/monicaribeiro/arquiteturahexagonal/config/exception/CustomExceptionHandler.java
package com.monicaribeiro.arquiteturahexagonal.config.exception;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.util.List;
import java.util.stream.Collectors;

@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpHeaders headers,
            HttpStatus status, WebRequest request) {

        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(x -> x.getDefaultMessage())
                .collect(Collectors.toList());

        ApiErrorMessage apiErrorMessage = new ApiErrorMessage(status, errors);

        return new ResponseEntity<>(apiErrorMessage, apiErrorMessage.getStatus());
    }
    
}

Temos algumas observações importantes a serem feitas:

  • Logo em cima do nome da nossa classe, temos a anotação @ControllerAdvice, que é de suma importância para o funcionamento conforme explicado no início do artigo;
  • A classe CustomExceptionHandler estende o comportamento da ResponseEntityExceptionHandler, que é uma classe base super conveniente quando implementamos esse tratamento de exceção centralizado, pois ele fornece métodos para lidar com exceções internas do Spring MVC;
  • Estamos sobrescrevendo (utilizando o @Override) uma tratativa da exceção MethodArgumentNotValidException (que é lançado quando a pessoa usuária desrespeita alguma validação aplicada no objeto de requisição aguardado) para pegar as informações lançadas na exceção e retornar elas dentro do objeto ApiErrorMessage criado anteriormente.

Além desses itens citados acima, é importante relembrar que no controller é necessário utilizar a anotação @Valid no contrato do método antes do parâmetro que está sendo recebido para que o controller entenda que precisa validar algo (quem nunca passou raiva com as exceções não sendo lançadas com as validações e depois foi ver e faltava essa anotação, não é mesmo? kkkrying)

Com essa implementação feita, a resposta da sua API com um corpo de requisição que desrespeite todas as regras seria:

Status: 400 BAD REQUEST
Resposta:
{
    "status": "BAD_REQUEST",
    "errors": [
        "Favorite movie name is mandatory.",
        "Name is mandatory."
    ]
}

Lembrando que esse é apenas um exemplo e que você pode personalizar segundo as necessidades da SUA aplicação.

Passo 3: implementação da exceção UserNotFoundException para lidar quando não encontrar users no banco de dados e adição da tratativa no CustomExceptionHandler

Na aplicação que estou citando como exemplo, tem uma integração com o banco de dados para buscar uma pessoa usuária por id. Caso ela não seja encontrada, quero que uma exceção personalizada seja lançada com a mensagem descrevendo a situação. Primeiro, vamos criar nossa exceção:

com/monicaribeiro/arquiteturahexagonal/config/exception/UserNotFoundException.java
package com.monicaribeiro.arquiteturahexagonal.config.exception;

public class UserNotFoundException extends RuntimeException {

    public UserNotFoundException(Long id) {
        super(String.format("User with id %d not found.", id));
    }

}

Nessa classe, podemos observar alguns pontos:

  • Ela estende o comportamento da superclasse RuntimeException. Caso você queira entender melhor sobre os três tipos possíveis de exceções, sugiro a leitura dessa documentação da Oracle.
  • Temos apenas um construtor, recebendo o id da pessoa usuária que não foi encontrado e utilizando o construtor da superclasse que recebe apenas uma mensagem (a qual está sendo definida).

Com essa exceção implementada, podemos adicionar a tratativa na classe CustomExceptionHandler:

com/monicaribeiro/arquiteturahexagonal/config/exception/CustomExceptionHandler.java

package com.monicaribeiro.arquiteturahexagonal.config.exception;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.util.List;
import java.util.stream.Collectors;

@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {

@ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<Object> handleUserNotFoundException(
            UserNotFoundException exception, WebRequest request) {

        ApiErrorMessage apiErrorMessage = new ApiErrorMessage(HttpStatus.NOT_FOUND, exception.getMessage());

        return new ResponseEntity<>(apiErrorMessage, new HttpHeaders(), apiErrorMessage.getStatus());
    }
}

O que podemos extrair de aprendizado dessa tratativa adicionada?

  • Antes do nosso método handleUserNotFoundException, adicionamos a anotação @ExceptionHandler. A configuração do Spring detectará essa anotação e registrará o método como manipulador de exceção para a classe de argumento e suas subclasses.
  • No corpo do método, estamos apenas recolhendo as informações necessárias da exceção e transformando o nosso objeto de retorno de erros ApiErrorMessage.

Antes de mostrar como fica o retorno para a pessoa usuária, é importante mostrar onde que estamos lançando o UserNotFoundException no nosso código:

com/monicaribeiro/arquiteturahexagonal/adapter/outbound/repository/GetUserByIdAdapter.java

package com.monicaribeiro.arquiteturahexagonal.adapter.outbound.repository;

import com.monicaribeiro.arquiteturahexagonal.config.exception.UserNotFoundException;
import com.monicaribeiro.arquiteturahexagonal.domain.domain.User;
import com.monicaribeiro.arquiteturahexagonal.domain.ports.outbound.GetUserByIdAdapterPort;
import org.springframework.stereotype.Component;

@Component
public class GetUserByIdAdapter implements GetUserByIdAdapterPort {

    private final UserRepository userRepository;

    public GetUserByIdAdapter(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public User getUser(Long id) {
        try {
            var userResult = userRepository.findById(id);
            return User.fromEntity(userResult.get());

        } catch (Exception exception) {
            throw new UserNotFoundException(id);
        }
    }
}

Dentro da nossa classe GetUserByIdAdapter, temos o método getUser() responsável por buscar pessoas usuárias através do repository e também de criar uma tratativa de erros. Caso retorne qualquer exceção, ele vai estar tratando isso e lançando o UserNotFoundException informando o id consultado.

Dessa forma, em caso de erro, essa será a visibilidade da resposta da nossa API para a pessoa usuária:

Status: 404 NOT FOUND
Resposta:
{
    "status": "NOT_FOUND",
    "errors": [
        "User with id 12 not found."
    ]
}

That’s all, folks!!

Com isso, concluímos o nosso exemplo de como criar tratativas globais para exceções utilizando @ControllerAdvice, retornando um objeto padronizado nos erros e também criando nossas próprias exceções. 

É uma abordagem muito aplicada em projetos pela sua facilidade de manutenção, uma vez que todas as exceções estão sendo tratadas em uma única classe.

O projeto completo está disponível neste repositório, vale a pena conferir!

Continue aprendendo com a gente aqui na nossa Central de Conteúdos! Temos artigos sobre back-end, front-end, dados, diversidade, inovação, entre outros temas!

@ControllerAdvice: pessoa sentada em uma mesa de frente para tela do computador. Além disso ao seu lado direito possui outra tela, e codificação de sites funcionando.
5ecc128c9deedd561f90700c_monica-ribeiro
Backend Developer
Inspirada em transformar o complexo em algo simples. Sou criadora de conteúdo no Instagram @monicaintech, amo contribuir em comunidades e congelar momentos em fotografias.

Artigos relacionados

Capa do artigo em foto com duas pessoas escrevendo códigos em frente a dois notebooks.
Back-End
Postado em:
Imagem capa do conteúdo sobre Framework NestJS com a foto de uma mulher de cabelos trançados de costas de frente para um computador com códigos.
Back-End
Postado em:
Imagem capa do conteúdo sobre testes unitários, onde uma pessoa branca está em pé, segurando um notebook aberto dentro de um data center.
Back-End
Postado em:

Este site utiliza cookies para proporcionar uma experiência de navegação melhor. Consulte nossa Política de Privacidade.