Bean Validation é uma especificação Java para tratar validações de dados de forma centralizada, pois as validações são inseridas no próprio modelo através de anotações, assim possibilitando a consistência das informações em diferentes camadas além de possuir integrações com outras especificações como JSF, JPA, Hibernate e JAX-RS.
O Bean Validation já fornece uma série de constraints como: @NotNull, @Size, @Min, @Max, entre outros, no entanto, nem sempre essas anotações padrões resolvem nosso problemas do dia a dia, com isso vou demonstrar um exemplo de como criar uma validação customizada para garantir que um atributo seja apto para ser convertido para um tipo BigDecimal.
O primeiro passo é criar uma anotação para marcar o atributo para validação e configurar algumas propriedades.
import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({FIELD}) @Retention(RUNTIME) @Constraint(validatedBy = StringAsBigDecimalValidator.class) public @interface StringAsBigDecimalValid { String message() default "Valor inválido"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String value() default ""; }
As anotações dão as seguintes características:
- @Target({FIELD}): A anotação apenas pode ser utilizada em fields, ou seja, em variáveis globais da classe;
- @Retention(RUNTIME): Essa validação é apenas em tempo de execução;
- @Constraint(validatedBy = StringAsBigDecimalValidator.class): Define que a classe StringAsBigDecimalValidator será responsável pela implementação da validação.
Os atributos message, groups, payload e value são os padrões das anotações de Bean Validation, onde tem os seguintes objetivos:
- message: A mensagem emitida em caso de erro na validação;
- groups: Para utilização de grupo de validações de Bean Validation;
- payloads: Para configurar o grau do erro de validação;
- value: É a propriedade que recebe o valor do atributo inserido.
Após criado a anotação para invocar a validação, é preciso implementar regra de validação, que é feita em uma classe concreta StringAsBigDecimalValidator que foi referenciada na anotação @Constraint.
A classe de validação implementa ConstraintValidator e precisa sobrescrever o método isValid, que o método que vai considerar se o valor recebido através do parâmetro value é valido ou não.
import com.google.common.base.Strings; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class StringAsBigDecimalValidator implements ConstraintValidator<StringAsBigDecimalValid, String> { private String value; @Override public void initialize(StringAsBigDecimalValid constraintAnnotation) { this.value = constraintAnnotation.value(); } @Override public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { if(Strings.isNullOrEmpty(value)) { return true; } try { new BigDecimal(value); return true; } catch (NumberFormatException nfex) { return false; } } }
No caso do exemplo, caso o valor vir nulo ou vazio é considerado válido, porque ele não é obrigatório, mas caso vir um valor deve ser possível criar um BigDecimal através do construtor, senão será lançado uma NumberFormatException e classificado o valor como inválido.
Finalizado as implementações, agora é apenas utilizar a anotação no atributo desejado.
import org.hibernate.validator.constraints.NotEmpty; public class ExcelMapper { @NotEmpty(message = "O código é obrigatório") private String codigo; @StringAsBigDecimalValid(message = "Valor inválido") private String valor; public ExcelMapper(String codigo, String valor) { this.codigo = codigo; this.valor = valor; } .... }
Para enfatizar o funcionamento da anotação StringAsBigDecimalValid, criei três casos de testes para validar o cenário de validação.
import org.junit.Test; import org.junit.Assert; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; public class ExcelMapperTest { final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); // Validação Ok, a String 10 é um valor válido de BigDecimal @Test public void naoDeveRetornarErroComLinhaComCodigoEValor() { final ExcelMapper linhaExcel = new ExcelMapper("1", "10"); final Set<ConstraintViolation<ExcelMapper>> constraintViolations = validator.validate(linhaExcel) Assert.assertTrue(constraintViolations.isEmpty()); } // Validação Ok, a validação não considera null como inválido @Test public void naoDeveRetornarErroComLinhaComApenasCodigo() { final ExcelMapper linhaExcel = new ExcelMapper("2", null); final Set<ConstraintViolation<ExcelMapper>> constraintViolations = validator.validate(linhaExcel) Assert.assertTrue(constraintViolations.isEmpty()); } // Erro de validação, a String AAA não é um valor válido de BigDecimal, pois não é um decimal @Test public void deveRetornarErroComValorInvalido() { final ExcelMapper linhaExcel = new ExcelMapper("3", "AAAA"); final Set<ConstraintViolation<ExcelMapper>> constraintViolations = validator.validate(linhaExcel) Assert.assertFalse(constraintViolations.isEmpty()); Assert.assertEquals(1, constraintViolations.size()); Assert.assertEquals("Valor inválido", constraintViolations.stream().findAny().get().getMessage()); } }
Observação: É possível instanciar um Validator através da factory buildDefaultValidatorFactory para simular uma validação de constraints de Bean Validations.
Com isso, é possível ver a simplicidade de criar nossas validações customizadas no Bean Validation, apenas criando uma anotação para marcar os atributos para validação e uma classe para implementar a validação podemos reutilizar essa garantia de consistência de dados em qualquer bean.
Show de bola EMMANUEL! Muito obrigado por compartilhar e por nos ajudar!
Eu estava procurando exatamente como fazer essa validações via Custom Annotations. Preciso importar um excel e queria incluir os padrões de validação desse modo, para ficar mais clean, organizado e dinâmico, ao invés de usar um monte de if/else no meu código rsrs
Abraços, sucesso!
CurtirCurtir
Obrigado Douglas! Fico muito feliz em poder ajudar! Abraço, sucesso!
CurtirCurtir
Parabéns pelo conteúdo.
CurtirCurtir