26
Validando requisições com Spring Boot
Erros ocorrem e sempre irão ocorrer, seja por mal requisito de negócio, seja por mal desenvolvimento ou qualquer outra razão. O fato é que erros acontecem e precisamos saber lidar com eles.
Mas como vamos apresentar o erro para o usuário ou cliente que chamam as nossas APIs?
O Spring já fornece todo um mecanismo para tratativa de erros que é muito válido e auxilia, e muito, o desenvolvimento mas aqui eu irei apresentar uma outra forma de realizar essa tratativa através de uma lib que descobri recentemente que facilita ainda mais o trabalho e padronização de erros e mensagens na resposta.
Seguindo com os conteúdos dos artigos anteriores vamos reaproveitar o projeto que já foi usado pra Criar um endpoint com Spring Security e token JWT e vamos adicionar nele a validação.
Este projeto também está sendo usado nos artigos sobre o Kafka aqui do blog e nessa aplicação temos como premissa que seja enviado os dados de um contribuinte como nome, documento e email.
No nosso projeto hoje não há validação alguma se os dados enviados estão corretos, para esse exemplo queremos que o número do documento (CPF), seja um número válido e caso não seja deve retornar uma mensagem indicando isso.
Para isso iremos usar uma lib muito interessante que ajuda muito no desenvolvimento com Spring Boot , a Errors Spring Boot Starter é um projeto que visa facilitar ainda mais a manipulação de erros e validação de dados de entrada.
O projeto register é um projeto Java que utiliza o Maven então basta adicionar a dependência no pom.xml
:
<dependency>
<groupId>me.alidg</groupId>
<artifactId>errors-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
E para auxiliar nas validações também incluir a dependência do spring-boot-starter-validation
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
O nosso objeto de entrada no POST contém um campo documento que será onde usuário irá enviar o CPF:
{
"name": "User",
"document": "XXX.XXX.XXX-XX",
"email": "[email protected]"
}
E a ideia seria que no momento do POST fosse realizada a validação e caso não seja um CPF válido retorna um erro informando isso.
Para isso será criado uma Annotation @Cpf
que nos auxiliará:
@Documented
@Constraint(validatedBy = CpfValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Cpf {
String message() default "Documento Inválido";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Na annotation @Cpf
é definida a mensagem que será devolvida para o usuário e também adicionamos @Constraint(validatedBy = CpfValidator.class)
que é classe que contém a lógica para executar a validação.
É necessário criar a classe CpfValidator que contém a regra de validação:
public class CpfValidator implements ConstraintValidator<Cpf, String>{
private final int[] PESO_CPF = { 11, 10, 9, 8, 7, 6, 5, 4, 3, 2 };
@Override
public boolean isValid(String cpf, ConstraintValidatorContext context) {
String cpfSomenteDigitos = cpf.replaceAll("\\D", "");
if ((cpfSomenteDigitos == null) || (cpfSomenteDigitos.length() != 11) || cpfSomenteDigitos.equals("00000000000")
|| cpfSomenteDigitos.equals("11111111111") || cpfSomenteDigitos.equals("22222222222")
|| cpfSomenteDigitos.equals("33333333333") || cpfSomenteDigitos.equals("44444444444")
|| cpfSomenteDigitos.equals("55555555555") || cpfSomenteDigitos.equals("66666666666")
|| cpfSomenteDigitos.equals("77777777777") || cpfSomenteDigitos.equals("88888888888")
|| cpfSomenteDigitos.equals("99999999999")) {
return false;
}
Integer digito1 = calcularDigito(cpfSomenteDigitos.substring(0, 9), PESO_CPF);
Integer digito2 = calcularDigito(cpfSomenteDigitos.substring(0, 9) + digito1, PESO_CPF);
return cpfSomenteDigitos.equals(cpfSomenteDigitos.substring(0, 9) + digito1.toString() + digito2.toString());
}
private int calcularDigito(String str, int[] peso) {
int soma = 0;
for (int indice = str.length() - 1, digito; indice >= 0; indice--) {
digito = Integer.parseInt(str.substring(indice, indice + 1));
soma += digito * peso[peso.length - str.length() + indice];
}
soma = 11 - soma % 11;
return soma > 9 ? 0 : soma;
}
}
A primeira coisa a se notar nessa classe é que ela implementa a interface do javax.validation ConstraintValidator que recebe uma Annotation como parâmetro.
E aqui executamos o algoritmo para validarmos se um CPF é válido ou não.
Para que a validação tenha efeito é necessário adicionar no Controller que recebe o nosso objeto a anotação @Valid
para que surja efeito.
public ResponseEntity<TaxpayerDTO> postTaxpayer(@Valid @RequestBody TaxpayerDTO taxpayer)
Com isso nós podemos adicionar essa Annotation ao atributo document
no DTO TaxpayerDTO.
@Cpf
private String document;
Se tentarmos agora fazer um POST com um número que não seja um CPF válido será lançado o erro com status 400.
{
"errors": [
{
"code": "Documento invalido",
"message": null
}
]
}
Já retorna uma mensagem de erro mais padronizada porém um tanto quanto estranha já que a message está com valor null
e no campo code está com a mensagem que definimos na Annotation CPF.
Isso ocorre pois a nossa dependência de validação usa o mecanismo do Spring MessageSource para buscar as mensagens que serão exibidas, então precisamos criar o nosso arquivo messages.properties
na pasta Resources do projeto e lá adicionamos as mensagens.
invalid.document=Documento invalido
E alteramos também na annotation com chave da mensagem.
String message() default "invalid.document";
Fazendo isso a tentando novamente com um CPF que é inválido o retorno será esse:
{
"errors": [
{
"code": "invalid.document",
"message": "Documento invalido"
}
]
}
Outra feature interessante dessa biblioteca é a capacidade de poder fazer a tratativa dos erros nas Exceptions que criamos na aplicação.
Vamos criar uma Exception para simular um cenário e poder ficar mais claro, imaginemos que há uma regra na nossa aplicação onde não seja aceito pessoas com o nome Guilherme e vamos verificar isso na nossa classe que executa as regras de negócio:
@Override
public void send(CommonDTO taxpayerDTO) {
TaxPayer taxPayer = TaxPayer.newBuilder().setName(((TaxpayerDTO) taxpayerDTO).getName())
.setDocument(((TaxpayerDTO) taxpayerDTO).getDocument()).setSituation(false).setEmail(((TaxpayerDTO) taxpayerDTO).getEmail()).build();
// Aqui está o lançamento da Exception
if(taxPayer.getName().contains("Guilherme")) {
throw new BadTaxpayerUser(taxPayer.getName());
}
producer.send(this.createProducerRecord(taxPayer), (rm, ex) -> {
if (ex == null) {
log.info("Data sent with success!!!");
} else {
log.error("Fail to send message", ex);
}
});
producer.flush();
}
No trecho de código acima temos uma Exception que recebe o nome do taxpayer, e será nessa classe de Exception que será feita a manipulação do erro para o retorno da API :
@Getter
@ExceptionMapping(statusCode = HttpStatus.I_AM_A_TEAPOT, errorCode = "bad.user.message")
public class BadTaxpayerUser extends RuntimeException {
@ExposeAsArg(value = 0, name = "user")
private final String key;
public BadTaxpayerUser(String key) {
super(key);
this.key = key;
}
}
Na BadTaxpayerUser bastou adicionarmos uma anotação @ExceptionMapping
e nela passamos no campo statusCode
o código HTTP que deve ser retornado e no campo errorCode
é passado a chave da mensagem, que está em resources
, que deve ser exibida.
Antes de vermos a anotação @ExposeAsArg
iremos adicionar ao arquivo message.properties
a mensagem que deve ser exibida e vamos customizá-la dessa forma:
bad.user.message=O user {user} nao pode!!!
Com isso quando adicionarmos a anotação @ExposeAsArg
no atributo key da Exception temos que informar que o primeiro argumento que está entre chaves, {user}
, deve ser interpolado pelo valor a ser recebido na exceção.
Agora fazendo o teste e enviando um POST com essas informações, que foram geradas pelo Gerador de pessoas da 4Devs:
{
"name": "Guilherme Paulo Carlos Eduardo Dias",
"document": "893.475.166-51",
"email": "[email protected]"
}
Teremos como retorno o erro com status 418 I’m a teapot com o corpo da mensagem:
{
"errors": [
{
"code": "bad.user.message",
"message": "O user Guilherme Paulo Carlos Eduardo Dias nao pode!!!"
}
]
}
Bom espero nesse artigo ter apresentando um pouco mais sobre a biblioteca Errors Spring Boot Starter e suas facilidades. Lembrando que essa é apenas uma outra maneira para lidar com validações.
O código desse projeto pode ser encontrado no GitHub
26