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:
- classe responsável pelo gerenciamento global das exceções utilizando a anotação @ControllerAdvice;
- se necessário, classes para representar exceções internas;
- (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:
Vamos à implementação de todos esses conceitos?
O exemplo prático disso tudo será dividido em três passos:
- definição da classe ApiErrorMessage que representará como nossos erros chegarão às pessoas usuárias;
- implementação da classe CustomExceptionHandler para lidar inicialmente com erros de argumentos não válidos recebidos pelo UserController;
- 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!