Restrições de método com a validação de bean 2.0

Restrições de método com a validação de bean 2.0

1. Visão geral

Neste artigo, discutiremos como definir e validar restrições de método usando o Bean Validation 2.0 (JSR-380).

Emthe previous article, discutimos JSR-380 com suas anotações embutidas e como implementar a validação de propriedade.

Aqui, vamos nos concentrar nos diferentes tipos de restrições de método, como:

  • restrições de parâmetro único

  • parâmetro cruzado

  • restrições de retorno

Além disso, veremos como validar as restrições manualmente e automaticamente usando o Spring Validator.

Para os exemplos a seguir, precisamos exatamente das mesmas dependências deJava Bean Validation Basics.

2. Declaração de restrições de método

Para começar,we’ll first discuss how to declare constraints on method parameters and return values of methods.

Como mencionado antes, podemos usar anotações dejavax.validation.constraints, mas também podemos especificar restrições personalizadas (e. g. para restrições personalizadas ou restrições de parâmetro cruzado).

2.1. Restrições de parâmetro único

Definir restrições em parâmetros únicos é simples. We simply have to add annotations to each parameter as required:

public void createReservation(@NotNull @Future LocalDate begin,
  @Min(1) int duration, @NotNull Customer customer) {

    // ...
}

Da mesma forma, podemos usar a mesma abordagem para construtores:

public class Customer {

    public Customer(@Size(min = 5, max = 200) @NotNull String firstName,
      @Size(min = 5, max = 200) @NotNull String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    // properties, getters, and setters
}

2.2. Usando Restrições de Parâmetros Cruzados

Em alguns casos, pode ser necessário validar vários valores de uma só vez, por exemplo, duas quantidades numéricas sendo uma maior que a outra.

Para esses cenários, podemos definir restrições personalizadas entre parâmetros, que podem depender de dois ou mais parâmetros.

Cross-parameter constraints can be considered as the method validation equivalent to class-level constraints. Poderíamos usar os dois para implementar a validação com base em várias propriedades.

Vamos pensar em um exemplo simples: uma variação do métodocreateReservation() da seção anterior leva dois parâmetros do tipoLocalDate: uma data de início e uma data de término.

Conseqüentemente, queremos ter certeza de quebegin está no futuro eend está depois debegin. Ao contrário do exemplo anterior, não podemos definir isso usando restrições de parâmetro único.

Em vez disso, precisamos de uma restrição de parâmetro cruzado.

Em contraste com as restrições de parâmetro único,cross-parameter constraints are declared on the method or constructor:

@ConsistentDateParameters
public void createReservation(LocalDate begin,
  LocalDate end, Customer customer) {

    // ...
}

2.3. Criação de restrições de parâmetro cruzado

Para implementar a restrição@ConsistentDateParameters, precisamos de duas etapas.

Primeiro, precisamosdefine the constraint annotation:

@Constraint(validatedBy = ConsistentDateParameterValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ConsistentDateParameters {

    String message() default
      "End date must be after begin date and both must be in the future";

    Class[] groups() default {};

    Class[] payload() default {};
}

Aqui, essas três propriedades são obrigatórias para anotações de restrição:

  • message – retorna a chave padrão para a criação de mensagens de erro, o que nos permite usar a interpolação de mensagens

  • groups - nos permite especificar grupos de validação para nossas restrições

  • payload - pode ser usado por clientes da API Bean Validation para atribuir objetos de carga útil personalizados a uma restrição

Para obter detalhes sobre como definir uma restrição personalizada, dê uma olhada emthe official documentation.

Depois disso, podemos definir a classe validadora:

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConsistentDateParameterValidator
  implements ConstraintValidator {

    @Override
    public boolean isValid(
      Object[] value,
      ConstraintValidatorContext context) {

        if (value[0] == null || value[1] == null) {
            return true;
        }

        if (!(value[0] instanceof LocalDate)
          || !(value[1] instanceof LocalDate)) {
            throw new IllegalArgumentException(
              "Illegal method signature, expected two parameters of type LocalDate.");
        }

        return ((LocalDate) value[0]).isAfter(LocalDate.now())
          && ((LocalDate) value[0]).isBefore((LocalDate) value[1]);
    }
}

Como podemos ver, o métodoisValid() contém a lógica de validação real. Primeiro, nos certificamos de obter dois parâmetros do tipoLocalDate.. Depois disso, verificamos se ambos estão no futuro e seend está apósbegin.

Além disso, é importante notar que a anotação@SupportedValidationTarget(ValidationTarget.PARAMETERS) na classeConsistentDateParameterValidator é necessária. A razão para isso é porque@ConsistentDateParameter é definido no nível do método, mas as restrições devem ser aplicadas aos parâmetros do método (e não ao valor de retorno do método, como discutiremos na próxima seção).

Nota: a especificação Bean Validation recomenda considerar os valoresnull como válidos. Senull não for um valor válido, a anotação@NotNull deve ser usada.

2.4. Restrições de valor de retorno

Às vezes, precisamos validar um objeto conforme ele é retornado por um método. Para isso, podemos usar restrições de valor de retorno.

O exemplo a seguir usa restrições internas:

public class ReservationManagement {

    @NotNull
    @Size(min = 1)
    public List<@NotNull Customer> getAllCustomers() {
        return null;
    }
}

ParagetAllCustomers(), as seguintes restrições se aplicam:

  • Primeiro, a lista retornada não deve sernulle deve ter pelo menos uma entrada

  • Além disso, a lista não deve conter entradasnull

2.5. Restrições personalizadas de valor de retorno

Em alguns casos, também podemos precisar validar objetos complexos:

public class ReservationManagement {

    @ValidReservation
    public Reservation getReservationsById(int id) {
        return null;
    }
}

Neste exemplo, um objetoReservation retornado deve satisfazer as restrições definidas por@ValidReservation, que definiremos a seguir.

Novamente,we first have to define the constraint annotation:

@Constraint(validatedBy = ValidReservationValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ValidReservation {
    String message() default "End date must be after begin date "
      + "and both must be in the future, room number must be bigger than 0";

    Class[] groups() default {};

    Class[] payload() default {};
}

Depois disso, definimos a classe validadora:

public class ValidReservationValidator
  implements ConstraintValidator {

    @Override
    public boolean isValid(
      Reservation reservation, ConstraintValidatorContext context) {

        if (reservation == null) {
            return true;
        }

        if (!(reservation instanceof Reservation)) {
            throw new IllegalArgumentException("Illegal method signature, "
            + "expected parameter of type Reservation.");
        }

        if (reservation.getBegin() == null
          || reservation.getEnd() == null
          || reservation.getCustomer() == null) {
            return false;
        }

        return (reservation.getBegin().isAfter(LocalDate.now())
          && reservation.getBegin().isBefore(reservation.getEnd())
          && reservation.getRoom() > 0);
    }
}

2.6. Valor de retorno em construtores

Como definimosMETHODeCONSTRUCTOR comotarget dentro de nossa interfaceValidReservation antes,we can also annotate the constructor of Reservation to validate constructed instances:

public class Reservation {

    @ValidReservation
    public Reservation(
      LocalDate begin,
      LocalDate end,
      Customer customer,
      int room) {
        this.begin = begin;
        this.end = end;
        this.customer = customer;
        this.room = room;
    }

    // properties, getters, and setters
}

2.7. Validação em Cascata

Por fim, a API de Validação de Bean nos permite validar não apenas objetos únicos, mas também gráficos de objetos, usando a chamada validação em cascata.

Hence, we can use @Valid for a cascaded validation, if we want to validate complex objects. Isso funciona para parâmetros de método e também para valores de retorno.

Vamos supor que temos uma classeCustomer com algumas restrições de propriedade:

public class Customer {

    @Size(min = 5, max = 200)
    private String firstName;

    @Size(min = 5, max = 200)
    private String lastName;

    // constructor, getters and setters
}

Uma classeReservation pode ter uma propriedadeCustomer, bem como outras propriedades com restrições:

public class Reservation {

    @Valid
    private Customer customer;

    @Positive
    private int room;

    // further properties, constructor, getters and setters
}

Se agora fizermos referência aReservation como um parâmetro de método,we can force the recursive validation of all properties:

public void createNewCustomer(@Valid Reservation reservation) {
    // ...
}

Como podemos ver, usamos@Valid em dois lugares:

  • No parâmetroreservation: dispara a validação do objetoReservation, quandocreateNewCustomer() é chamado

  • Como temos um gráfico de objeto aninhado aqui, também temos que adicionar um@Valid no atributocustomer: assim, ele aciona a validação desta propriedade aninhada

Isso também funciona para métodos que retornam um objeto do tipoReservation:

@Valid
public Reservation getReservationById(int id) {
    return null;
}

3. Validando as restrições do método

Após a declaração de restrições na seção anterior, agora podemos prosseguir para validar essas restrições. Para isso, temos várias abordagens.

3.1. Validação Automática com Spring

Spring Validation fornece uma integração com o Hibernate Validator.

Nota: A validação do Spring é baseada no AOP e usa o Spring AOP como a implementação padrão. Portanto, a validação funciona apenas para métodos, mas não para construtores.

Se agora queremos que o Spring valide nossas restrições automaticamente, precisamos fazer duas coisas:

Firstly, we have to annotate the beans, which shall be validated, with @Validated:

@Validated
public class ReservationManagement {

    public void createReservation(@NotNull @Future LocalDate begin,
      @Min(1) int duration, @NotNull Customer customer){

        // ...
    }

    @NotNull
    @Size(min = 1)
    public List<@NotNull Customer> getAllCustomers(){
        return null;
    }
}

Em segundo lugar, temos que fornecer um beanMethodValidationPostProcessor:

@Configuration
@ComponentScan({ "org.example.javaxval.methodvalidation.model" })
public class MethodValidationConfig {

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

O contêiner agora irá lançar umjavax.validation.ConstraintViolationException, se uma restrição for violada.

Se estivermos usando Spring Boot, o contêiner registrará um beanMethodValidationPostProcessor para nós, desde quehibernate-validator esteja no classpath.

3.2. Validação automática com CDI (JSR-365)

A partir da versão 1.1, a Validação de Bean trabalha com CDI (Contextos e Injeção de Dependências para Java EE).

Se nosso aplicativo for executado em um contêiner Java EE, o contêiner validará as restrições do método automaticamente no momento da chamada.

3.3. Validação Programática

Paramanual method validation in a standalone Java application, podemos usar a interfacejavax.validation.executable.ExecutableValidator.

Podemos recuperar uma instância usando o seguinte código:

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
ExecutableValidator executableValidator = factory.getValidator().forExecutables();

ExecutableValidator oferece quatro métodos:

  • validateParameters() evalidateReturnValue() para validação do método

  • validateConstructorParameters() evalidateConstructorReturnValue() para validação do construtor

A validação dos parâmetros do nosso primeiro métodocreateReservation() ficaria assim:

ReservationManagement object = new ReservationManagement();
Method method = ReservationManagement.class
  .getMethod("createReservation", LocalDate.class, int.class, Customer.class);
Object[] parameterValues = { LocalDate.now(), 0, null };
Set> violations
  = executableValidator.validateParameters(object, method, parameterValues);

Nota: A documentação oficial desencoraja chamar esta interface diretamente do código do aplicativo, mas usá-la por meio de uma tecnologia de interceptação de método, como AOP ou proxies.

Caso você esteja interessado em como usar a interfaceExecutableValidator, você pode dar uma olhada emofficial documentation.

4. Conclusão

Neste tutorial, vimos rapidamente como usar restrições de método com o Hibernate Validator e também discutimos alguns novos recursos do JSR-380.

Primeiro, discutimos como declarar diferentes tipos de restrições:

  • Restrições de parâmetro único

  • Parâmetro cruzado

  • Restrições de valor de retorno

Também vimos como validar as restrições manual e automaticamente usando o Spring Validator.

Como sempre, o código-fonte completo dos exemplos está disponívelover on GitHub.