Contraintes de méthode avec Bean Validation 2.0

1. Vue d'ensemble

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

Dans l'article précédent, nous avons abordé JSR-380 avec ses annotations intégrées et comment 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 à un seul paramètre
  • 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 dans Java Bean Validation Basics.

2. Déclaration des contraintes de méthode

Pour commencer, nous allons d'abord discuter de la façon de déclarer des contraintes sur les paramètres de méthode et de renvoyer les valeurs des méthodes .

Comme mentionné précédemment, nous pouvons utiliser des annotations de javax.validation.constraints , mais nous pouvons également spécifier des contraintes personnalisées (par exemple pour des contraintes personnalisées ou des contraintes inter-paramètres).

2.1. Contraintes de paramètre unique

La définition de contraintes sur des paramètres uniques est simple. Nous devons simplement ajouter des annotations à chaque paramètre selon les besoins :

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, nous pouvons avoir besoin 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 inter-paramètres personnalisées, qui peuvent dépendre de deux paramètres ou plus.

Les contraintes croisées peuvent être considérées comme la validation de méthode équivalente aux contraintes de niveau classe . Nous pourrions utiliser les deux pour implémenter la validation basée sur plusieurs propriétés.

Pensons à un exemple simple: une variante de la méthode createReservation () de la section précédente prend deux paramètres de type LocalDate: une date de début et une date de fin.

Par conséquent, nous voulons nous assurer que le début est dans le futur et que la fin est après le début . Contrairement à l'exemple précédent, nous ne pouvons pas définir cela en utilisant des contraintes de paramètre unique.

Au lieu de cela, nous avons besoin d'une contrainte inter-paramètres.

Contrairement aux contraintes à un seul paramètre, les contraintes croisées sont déclarées sur la méthode ou le constructeur :

@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 devons définir l'annotation de contrainte :

@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 contraintes:

  • 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
  • charge utile - peut être utilisée par les clients de l'API Bean Validation pour attribuer des objets de charge utile personnalisés à une contrainte

Pour plus de détails sur la définition d'une contrainte personnalisée, consultez la documentation officielle.

Après cela, nous pouvons définir la classe du validateur:

@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éthode isValid () contient la logique de validation réelle. Tout d'abord, nous nous assurons d'obtenir deux paramètres de type LocalDate. Après cela, nous vérifions si les deux sont dans le futur et si la fin est après le début .

De plus, il est important de noter que l' annotation @SupportedValidationTarget (ValidationTarget . PARAMETERS) sur la classe ConsistentDateParameterValidator est requise. 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 valeurs nulles comme valides. Si null n'est pas une valeur valide, l' annotation @NotNull doit être utilisée à la place.

2.4. Contraintes de valeur de retour

Parfois, nous aurons besoin de 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 getAllCustomers() { return null; } }

Pour getAllCustomers () , les contraintes suivantes s'appliquent:

  • Premièrement, la liste retournée ne doit pas être nulle et doit avoir au moins une entrée
  • De plus, la liste ne doit pas contenir d' entrées nulles

2.5. Contraintes personnalisées de valeur de retour

Dans certains cas, nous pouvons également avoir besoin de valider des objets complexes:

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

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

Encore une fois, nous devons d'abord définir l'annotation de contrainte :

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

After that, we define the validator class:

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. Return Value in Constructors

As we defined METHOD and CONSTRUCTOR as target within our ValidReservation interface before, 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. Cascaded Validation

Finally, the Bean Validation API allows us to not only validate single objects but also object graphs, using the so-called cascaded validation.

Hence, we can use @Valid for a cascaded validation, if we want to validate complex objects. This works for method parameters as well as for return values.

Let's assume that we have a Customer class with some property constraints:

public class Customer { @Size(min = 5, max = 200) private String firstName; @Size(min = 5, max = 200) private String lastName; // constructor, getters and setters }

A Reservation class might have a Customer property, as well as further properties with constraints:

public class Reservation { @Valid private Customer customer; @Positive private int room; // further properties, constructor, getters and setters }

If we now reference Reservation as a method parameter, we can force the recursive validation of all properties:

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

As we can see, we use @Valid at two places:

  • On the reservation-parameter: it triggers the validation of the Reservation-object, when createNewCustomer() is called
  • As we have a nested object graph here, we also have to add a @Valid on the customer-attribute: thereby, it triggers the validation of this nested property

This also works for methods returning an object of type Reservation:

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

3. Validating Method Constraints

After the declaration of constraints in the previous section, we can now proceed to actually validate these constraints. For that, we have multiple approaches.

3.1. Automatic Validation With Spring

Spring Validation provides an integration with Hibernate Validator.

Note: Spring Validation is based on AOP and uses Spring AOP as the default implementation. Therefore, validation only works for methods, but not for constructors.

If we now want Spring to validate our constraints automatically, we have to do two things:

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 getAllCustomers(){ return null; } }

Secondly, we have to provide a MethodValidationPostProcessor bean:

@Configuration @ComponentScan({ "org.baeldung.javaxval.methodvalidation.model" }) public class MethodValidationConfig { @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); } }

The container now will throw a javax.validation.ConstraintViolationException, if a constraint is violated.

If we are using Spring Boot, the container will register a MethodValidationPostProcessor bean for us as long as hibernate-validator is in the classpath.

3.2. Automatic Validation With CDI (JSR-365)

As of version 1.1, Bean Validation works with CDI (Contexts and Dependency Injection for Jakarta EE).

If our application runs in a Jakarta EE container, the container will validate method constraints automatically at the time of invocation.

3.3. Programmatic Validation

For manual method validation in a standalone Java application, we can use the javax.validation.executable.ExecutableValidator interface.

We can retrieve an instance using the following code:

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

ExecutableValidator offers four methods:

  • validateParameters() and validateReturnValue() for method validation
  • validateConstructorParameters() and validateConstructorReturnValue() for constructor validation

Validating the parameters of our first method createReservation() would look like this:

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

Note: The official documentation discourages to call this interface directly from the application code, but to use it via a method interception technology, like AOP or proxies.

Si vous souhaitez utiliser l' interface ExecutableValidator , vous pouvez consulter la documentation officielle.

4. Conclusion

Dans ce didacticiel, nous avons examiné rapidement comment utiliser les contraintes de méthode avec Hibernate Validator, ainsi que quelques nouvelles fonctionnalités de JSR-380.

Tout d'abord, nous avons expliqué comment déclarer différents types de contraintes:

  • Contraintes de paramètre unique
  • Paramètres croisés
  • Contraintes de valeur 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 disponible à l'adresse over sur GitHub.