Contraintes de méthodes avec validation de bean 2.0

Contraintes de méthodes avec validation de bean 2.0

1. Vue d'ensemble

Dans cet article, nous expliquerons comment définir et valider les contraintes de méthode à l’aide de Bean Validation 2.0 (JSR-380).

Dansthe previous article, nous avons discuté du JSR-380 avec ses annotations intégrées et de la façon d'implémenter la validation de propriété.

Ici, nous allons nous concentrer sur les différents types de contraintes de méthode tels que:

  • contraintes mono-paramètres

  • paramètre croisé

  • contraintes de retour

Nous verrons également comment valider les contraintes manuellement et automatiquement à l'aide de Spring Validator.

Pour les exemples suivants, nous avons besoin exactement des mêmes dépendances que dansJava Bean Validation Basics.

2. Déclaration des contraintes de méthode

Pour commencer,we’ll first discuss how to declare constraints on method parameters and return values of methods.

Comme mentionné précédemment, nous pouvons utiliser des annotations dejavax.validation.constraints, mais nous pouvons également spécifier des contraintes personnalisées (e. g. pour des contraintes personnalisées ou des contraintes entre paramètres).

2.1. Contraintes de paramètre unique

Définir des contraintes sur des paramètres uniques est simple. 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) {

    // ...
}

De même, nous pouvons utiliser la même approche pour les constructeurs:

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. Utilisation de contraintes de paramètres croisés

Dans certains cas, il peut être nécessaire de valider plusieurs valeurs à la fois, par exemple deux montants numériques étant l’un plus grand que l’autre.

Pour ces scénarios, nous pouvons définir des contraintes personnalisées de paramètres croisés, qui peuvent dépendre de deux paramètres ou plus.

Cross-parameter constraints can be considered as the method validation equivalent to class-level constraints. Nous pourrions utiliser les deux pour implémenter une validation basée sur plusieurs propriétés.

Prenons un exemple simple: une variante de la méthodecreateReservation() de la section précédente prend deux paramètres de typeLocalDate: une date de début et une date de fin.

Par conséquent, nous voulons nous assurer quebegin est dans le futur et queend est aprèsbegin. Contrairement à l'exemple précédent, nous ne pouvons pas définir cela à l'aide de contraintes de paramètre unique.

Au lieu de cela, nous avons besoin d'une contrainte de paramètres croisés.

Contrairement aux contraintes à paramètre unique,cross-parameter constraints are declared on the method or constructor:

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

    // ...
}

2.3. Création de contraintes de paramètres croisés

Pour implémenter la contrainte@ConsistentDateParameters, nous avons besoin de deux étapes.

Tout d'abord, nous devonsdefine 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 {};
}

Ici, ces trois propriétés sont obligatoires pour les annotations de contrainte:

  • message – renvoie la clé par défaut pour créer des messages d'erreur, cela nous permet d'utiliser l'interpolation de message

  • groups - nous permet de spécifier des groupes de validation pour nos contraintes

  • payload - peut être utilisé par les clients de l'API Bean Validation pour affecter des objets de charge utile personnalisés à une contrainte

Pour plus de détails sur la définition d'une contrainte personnalisée, jetez un œil àthe official documentation.

Après cela, nous pouvons définir la classe de validation:

@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]);
    }
}

Comme nous pouvons le voir, la méthodeisValid() contient la logique de validation réelle. Tout d'abord, nous nous assurons d'obtenir deux paramètres de typeLocalDate. Après cela, nous vérifions si les deux sont dans le futur et siend est aprèsbegin.

De plus, il est important de noter que l'annotation@SupportedValidationTarget(ValidationTarget.PARAMETERS) sur la classeConsistentDateParameterValidator est obligatoire. La raison en est que@ConsistentDateParameter est défini au niveau de la méthode, mais les contraintes doivent être appliquées aux paramètres de la méthode (et non à la valeur de retour de la méthode, comme nous le verrons dans la section suivante).

Remarque: la spécification Bean Validation recommande de considérer les valeursnull comme valides. Sinull n’est pas une valeur valide, l’annotation@NotNull doit être utilisée à la place.

2.4. Contraintes de valeur de retour

Parfois, nous devrons valider un objet tel qu’il est renvoyé par une méthode. Pour cela, nous pouvons utiliser des contraintes de valeur de retour.

L'exemple suivant utilise des contraintes intégrées:

public class ReservationManagement {

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

PourgetAllCustomers(), les contraintes suivantes s'appliquent:

  • Tout d'abord, la liste retournée ne doit pas êtrenull et doit avoir au moins une entrée

  • De plus, la liste ne doit pas contenir d'entréesnull

2.5. Contraintes personnalisées de valeur de retour

Dans certains cas, il peut également être nécessaire de valider des objets complexes:

public class ReservationManagement {

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

Dans cet exemple, un objetReservation retourné doit satisfaire les contraintes définies par@ValidReservation, que nous définirons ensuite.

Encore une fois,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 {};
}

Après cela, nous définissons la classe validateur:

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. Valeur de retour dans les constructeurs

Comme nous avons définiMETHOD etCONSTRUCTOR commetarget dans notre interfaceValidReservation auparavant,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. Validation en cascade

Enfin, l’API de validation de bean nous permet non seulement de valider des objets isolés, mais également des graphiques d’objet, à l’aide de la validation dite en cascade.

Hence, we can use @Valid for a cascaded validation, if we want to validate complex objects. Cela fonctionne pour les paramètres de méthode ainsi que pour les valeurs de retour.

Supposons que nous ayons une classeCustomer avec quelques contraintes de propriété:

public class Customer {

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

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

    // constructor, getters and setters
}

Une classeReservation peut avoir une propriétéCustomer, ainsi que d'autres propriétés avec des contraintes:

public class Reservation {

    @Valid
    private Customer customer;

    @Positive
    private int room;

    // further properties, constructor, getters and setters
}

Si nous référençons maintenantReservation comme paramètre de méthode,we can force the recursive validation of all properties:

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

Comme nous pouvons le voir, nous utilisons@Valid à deux endroits:

  • Sur le paramètrereservation: il déclenche la validation de l'objetReservation, lorsquecreateNewCustomer() est appelé

  • Comme nous avons ici un graphe d'objets imbriqués, nous devons également ajouter un@Valid sur l'attribut scustomer: il déclenche ainsi la validation de cette propriété imbriquée

Cela fonctionne également pour les méthodes retournant un objet de typeReservation:

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

3. Validation des contraintes de méthode

Après la déclaration de contraintes dans la section précédente, nous pouvons maintenant procéder à la validation effective de ces contraintes. Pour cela, nous avons plusieurs approches.

3.1. Validation automatique avec Spring

Spring Validation fournit une intégration avec Hibernate Validator.

Remarque: Spring Validation est basé sur AOP et utilise Spring AOP comme implémentation par défaut. Par conséquent, la validation ne fonctionne que pour les méthodes, mais pas pour les constructeurs.

Si nous voulons maintenant que Spring valide automatiquement nos contraintes, nous devons faire deux choses:

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;
    }
}

Deuxièmement, nous devons fournir un beanMethodValidationPostProcessor:

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

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

Le conteneur lancera maintenant unjavax.validation.ConstraintViolationException, si une contrainte est violée.

Si nous utilisons Spring Boot, le conteneur enregistrera un beanMethodValidationPostProcessor pour nous tant quehibernate-validator est dans le chemin de classe.

3.2. Validation automatique avec CDI (JSR-365)

À partir de la version 1.1, la validation de bean fonctionne avec CDI (Contexts and Dependency Injection for Java EE).

Si notre application s'exécute dans un conteneur Java EE, ce dernier validera automatiquement les contraintes de méthode au moment de l'appel.

3.3. Validation programmatique

Pourmanual method validation in a standalone Java application, nous pouvons utiliser l'interfacejavax.validation.executable.ExecutableValidator.

Nous pouvons récupérer une instance en utilisant le code suivant:

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

ExecutableValidator propose quatre méthodes:

  • validateParameters() etvalidateReturnValue() pour la validation de méthode

  • validateConstructorParameters() etvalidateConstructorReturnValue() pour la validation du constructeur

La validation des paramètres de notre première méthodecreateReservation() ressemblerait à ceci:

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);

Remarque: La documentation officielle déconseille d'appeler cette interface directement depuis le code de l'application, mais de l'utiliser via une technologie d'interception de méthodes, comme AOP ou des proxies.

Si vous êtes intéressé par la façon d'utiliser l'interfaceExecutableValidator, vous pouvez jeter un œil auxofficial documentation.

4. Conclusion

Dans ce didacticiel, nous avons brièvement examiné comment utiliser les contraintes de méthode avec Hibernate Validator. Nous avons également présenté certaines nouvelles fonctionnalités de JSR-380.

Premièrement, nous avons discuté de la façon de déclarer différents types de contraintes:

  • Contraintes à paramètre unique

  • Paramètres croisés

  • Contraintes de retour

Nous avons également examiné comment valider les contraintes manuellement et automatiquement à l'aide de Spring Validator.

Comme toujours, le code source complet des exemples est disponibleover on GitHub.