Validando requisições e tratando exceções no Spring Boot

Mário Sérgio Esteves Alvial
7 min readApr 11, 2018

--

Homem pensando

Todo o código deste artigo está disponível no meu GitHub.

Usando o exemplo do post passado, temos uma aplicação para cadastro de usuários. Vamos olhar o método do controller que recebe um novo usuário para ser salvo:

Recebemos um objeto do tipo UsuarioDTO e chamamos o método salvar() do objeto usuarioService passando como parâmetro o nosso DTO transformado para objeto real, no caso, transformado em um objeto do tipo Usuario. Esse método nos devolve o usuário salvo que é transformado em um DTO de resposta e é enviado para o client.

Vamos testar para ver se está funcionando:

Ótimo, está funcionando perfeitamente.

Mas, pensando em uma aplicação real, em um formulário, quais as chances de um usuário esquecer de preencher algum desses campos? Quais as chances do seu front não validar todos os campos? Quais as chances de um usuário mal intencionado começar a enviar diversas requisições “quebradas”?

Durante a criação de uma API todos esses cenários, ou pelo menos a maioria deles, devem ser avaliados e sua aplicação deve ser capaz de conseguir lidar com os mesmos.

Vamos testar a mesma requisição, só que vamos tirar o campo email e ver o que acontece:

Tomamos um belo de um Internal Server Error. Geralmente esse erro significa que nós (desenvolvedores) fizemos algo errado ou deixamos de fazer algo.

Nesse caso, o erro aconteceu pois tentamos salvar no banco de dados um valor nulo para uma coluna que não pode ter valor nulo.

Perceba que isso aconteceu por não termos enviado um atributo, email. Mas poderia ter sido o nome ou a senha ou os dois.

Temos que preparar nossa aplicação para lidar com todos esses cenários. E agora? Quem poderá nos defender?

Validando campos com a especificação Bean Validation

Precisamos validar os atributos da nossa requisição antes de salvar nosso usuário. Para isso vamos usar o Hibernate Validator que é a implementação da especificação Bean Validation.

Beleza, mas que classe iremos validar? Como iremos validar? Vejamos, qual a primeira classe a receber os valores do corpo da nossa requisição? A UsuarioDTO, logo, se validarmos os atributos dela e a mesma possuir erros, não precisamos continuar a execução do nosso código.

Vamos olhar a classe UsuarioDTO para ver o que precisamos validar:

Beleza, temos nome, email e senha. Para que esses atributos sejam válidos eles não podem ser nulos, não podem estar em branco, ou seja, não podem ser Strings vazias, não podem ser compostos apenas por espaços em brancos e, no caso do campo email, precisa obrigatoriamente ter um ‘@’.

Olhe como essa validação é simples de se fazer:

As anotações, também chamadas de constraints, colocadas atuam sobre o atributo anotado. Detalhando cada anotação, temos:

  • @NotBlank: Verifica se o campo não está nulo ou vazio. Vale uma ressalva que antes de verificar se o campo está vazio um trim() é aplicado sobre o campo, logo Strings como " " não são permitidos.
  • @Email: Verifica se o campo possui as características de um endereço de e-mail.

Para finalizar, temos que adicionar a anotação @Valid no parâmetro do nosso controller. Essa anotação serve para indicar que o objeto será validado tendo como base as anotações de validação que atribuímos aos campos.

Ficamos com o método do controller da seguinte maneira:

Ótimo, tudo validado, vamos testar exatamente a mesma requisição e ver o resultado:

Bom… podemos perceber que a validação funcionou, nossa resposta mostra que o campo email recebeu um valor null e que a validação falhou pois o mesmo “não pode estar em branco”.

Mas olha para esta resposta, muito informação jogada, a estética dessa resposta não está nada boa e tem vários campos que eu não faço ideia do que são. Enfim, não é a resposta ideal.

Vamos começar a melhorar esta resposta. Primeiramente, mudaremos esta defaultMessage, cada mensagem deve explicitar escancaradamente o erro de validação que ocorreu.

Antes de tudo, precisamos entender que toda constraint precisa ter uma mensagem padrão, como não falamos que mensagem queremos vincular a nossa constraint o validador usou a padrão, no exemplo acima foi a “Não pode estar em branco”.

Para customizarmos nossas mensagens iremos criar um arquivo chamado ValidationMessages.properties dentro de src/main/resources.

O nome e o diretório do nosso arquivo não é por acaso, quando uma validação falhar, um interpolador, no caso o MessageInterpolator, verificará se um arquivo com este nome existe no classpath do projeto, caso exista, o mesmo tentará usar as mensagens do nosso arquivo.

Se alguma mensagem de validação não possuir um correspondente no nosso arquivo de mensagens, a mensagem padrão será usada.

Nosso projeto ficará assim:

Esse arquivo precisa ser escrito usando o tipo de armazenamento chave e valor, para que possamos chamar nossas mensagens por meio das chaves atribuídas as mesmas.

Você leitor pode escrever as mensagens da forma que achar melhor. As minhas mensagens ficaram assim:

Beleza, agora só precisamos falar para nossas constraints que caso alguma delas falhe deverá ser usado as mensagens do nosso arquivo.

Isso é simples de fazer, toda constraint possui um atributo chamado message. Basta atribuirmos a ele a chave da mensagem que queremos adicionar.

Adicionando as chaves ao atributo, nossa classe UsuarioDTO fica desta forma:

Novamente, vamos testar a mesma requisição e ver se a mensagem de erro para o campo email foi alterada:

Beleza, a mensagem mudou, mas ainda temos essa resposta enorme e confusa. Precisamos arrumar isso.

Manipulando exceções no Spring

Para atingir nosso objetivo precisamos capturar a exceção lançada pela falha na validação. Conseguindo fazer isso, podemos tratar essa exceção e enviar os erros de forma mais suave e concisa.

Para conseguir fazer essa captura primeiro vamos criar uma classe que será responsável por capturar e tratar esses erros:

Vamos anotar nossa classe com @RestControllerAdvice:

Com essa anotação estamos tornando nossa classe visível para o Spring. Estamos dizendo que ela é um componente especializado em tratar exceções e que o retorno dos métodos da mesma devem ser inserido no corpo da resposta HTTP e convertidos para JSON (como não especificamos nenhum tipo de conversão, o Spring, através do Jackson, usará a padrão, no caso, JSON).

Nesse caso, a exceção que queremos tratar é a MethodArgumentNotValidException que é a exceção lançada quando alguma validação de um argumento anotado com @Valid falha.

Lembra daquela resposta grande e confusa que recebemos ao começar a validar nossos campos? Então, ela já é a nossa exceção tratada, só que pelo Spring.

Para sobrescrever este comportamento podemos usar como base a classe ResponseEntityExceptionHandler, ela já possui vários métodos que tratam exceções para que o usuário tenha uma resposta mais completa a respeito do erro lançado, sendo cada método para uma exceção específica.

Então vamos estender esta classe para ter acesso a estes métodos prontos.

Quando alguma validação feita pelas anotações do Bean Validation falha é lançada a exceção MethodArgumentNotValidException. Logo, vamos sobrescrever o método que lida com essa exceção:

Vale uma breve explicação que a captura dessa exceção só é possível graças a anotação @ExceptionHandler que, neste caso, está na classe do Spring que estendemos. Essa anotação prove ao método a capacidade de tratar uma exceção quando ela for lançada. Para isso precisamos passar a classe da exceção como parâmetro da anotação e passar um objeto do tipo da exceção como parâmetro do método.

O tratamento em si da exceção pode ser feito de várias formas, para o nosso problema, irei criar uma classe, com um construtor que engloba todos os atributos, que fala da exceção de uma forma geral e que possui uma lista de erros específicos. Dessa forma:

Agora obtivemos um erro de compilação, pois a classe ErrorObject não existe. Vamos criar-lá:

Ótimo, temos nossas classes que irão representar a exceção. Voltando ao nosso método na classe RestExceptionHandler vamos criar nossa lista de ErrorObject:

Agora só nos resta criar o objeto ErrorResponse e nosso ResponseEntity:

Vamos testar e ver como nossa aplicação está respondendo a uma requisição inválida:

Bem melhor, não?

Informações Adicionais

Há vários outros métodos da classe `ResponseEntityExceptionHandler` que podem ser sobrescrito e, também, é possível tratar suas próprias exceções nesta classe que criamos. Basta apenas adicionar o @ExceptionHandler passando a sua exceção.

Muitos desenvolvedores encorajam a validação tanto no objeto que recebe a requisição quanto na própria classe, ou seja, neste caso na classe UsuarioDTO e na classe Usuario.

Ao meu ver, é uma prática importante quando se esta trabalhando em times grandes, pois isso garante que algum outro desenvolvedor não vá desqualificar seu atributo no meio da execução de algum código.

Conclusão

Uma API precisa estar pronta para os diversos cenários de caos que podem ocorrer e faz parte do trabalho do desenvolvedor lidar com tais problemas.

Uma boa API, ao se deparar com um erro, o trata e responde de forma detalhada a respeito do ocorrido.

E aí, o que achou do artigo? Não deixe de comentar caso tenha ficado alguma dúvida ou tenha alguma sugestão, tentarei responder o mais rápido possível.

--

--

Mário Sérgio Esteves Alvial
Mário Sérgio Esteves Alvial

Written by Mário Sérgio Esteves Alvial

Amante de pudim e programador em constante aprendizado. Apaixonado por código limpo e boas práticas de programação.

Responses (8)