Java: Vantagens de criar suas próprias constraints com Bean Validation

Recentemente, em um grande projeto que faço parte, precisei utilizar alguma implementação que validasse alguns dados de entrada em uma API REST. Pensei em fazer algo como um serviço de validação ou algo do tipo, que contivesse algumas lógicas para objeto a ser validado. Porém ainda bem que lembrei do Bean Validation do Java.

A integridade dos dados é uma parte importante da lógica da aplicação. O Bean Validation é uma API que fornece um recurso para validar objetos, membros de objetos, métodos e construtores. Essa API permite que os desenvolvedores restrinjam uma vez e validem em qualquer lugar.

O Bean Validation traz várias constraints internas, talvez as mais comuns usadas em aplicativos pequenos e grandes, alguns deles são: @NotNull, @Size, @Max, @Min, @Email.

Porém, quando as constraints internas não são suficientes para nossos aplicativos, podemos criar nossas próprias constraints que podem ser usadas em qualquer lugar que for necessário.

/**
 * @author João Faro
 * @version 1.1.0
 */
@Documented
@Constraint(validatedBy = CurrencyValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrecyConstraint {
    String message() default "Currency code is not valid";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

O código acima é uma annotation que criei para validar uma requisição que uma das variáveis era um código de moeda. Como as informações a respeito da currency code estavam armazenadas na base de dados, poderia simplesmente injetar um repository ou um service para tentar buscar esses dados de acordo com o código de moeda que foi passado na requisição, porém essa abordagem não seria o ideal, pois já temos uma api para fazer toda nossa validação inicial, justamente o Bean Validation.

Então vamos usar o Bean Validation!

Nesse caso não tem muito segredo, é semelhante a outros tipos de annotation. Para definir esse tipo de annotation como uma constraint de bean validation, precisamos adicionar a annotation javax.validation.Constraint (@Constraint) em sua declaração.

@Target é onde nossas annotations podem ser usadas.

@Retention especifica como a annotation marcada é armazenada.Escolhemos RUNTIME, para que possa ser usado pelo ambiente de tempo de execução.

A String message define a mensagem que será mostrada quando os dados de entrada não forem válidos.

Class[] Groups() permite que o desenvolvedor selecione dividir as annotations em grupos diferentes para aplicar validações diferentes a cada grupo, por exemplo, @Age (groups = MASCULINO).

Class[] payLoad(): Payloads geralmente são usados para transportar informações de metadados consumidas por um cliente de validação.

Agora vamos para a classe de validação: CurrencyValidator.class

/**
 * @author João Faro
 * @version 1.1.0
 */
public class CurrencyValidator implements ConstraintValidator<CurrecyConstraint, Integer> {

    @Autowired
    private CurrencyRepository currencyRepository;

    @Override
    public void initialize(CurrecyConstraint constraintAnnotation) {
    }

    @Override
    public boolean isValid(Integer code, ConstraintValidatorContext constraintValidatorContext) {
        Optional<Currency> currency = currencyRepository.findByNumericCode(code);
        return currency.isPresent();
    }
}

A classe de validação deve implementar ConstraintValidator. Ele tem dois métodos: initialize e isValid.

O método initialize será usado para estabelecer os atributos necessários para executar a validação – no meu caso, não precisei escrever nada.

isValid é o método que recebeu o valor de entrada e decide se é válido ou não. No meu caso eu utilizei o repository onde faço uma busca pelo código na base de dados me devolvendo um Optional (Já falei das vantagens de utilizar o Optional em um post passado, vale a pena dar uma conferida depois) e, por fim, verifico se o valor ali está presente ou não, caso não esteja devolve a mensagem que colocamos como default lá na annotation.

Ficaria mais ou menos assim no meu DTO:

 @CurrecyConstraint @NotEmpty @NotNull
    private Integer currency;

Bem legal né? É isso! Agora você está pronto para criar suas próprias constraints de Bean validation. Até o próximo artigo.