Bean Validation

Comment valider un bean ? L’idée de départ, comme toutes les bonnes idées, est très simple. Avant, pour confirmer que des données étaient valides selon certains critères métiers, le développeur pouvait être amené à intervenir sur plusieurs couches. Il pouvait agir, par exemple, sur la couche présentation, en ajoutant du javascript pour contrôler un champ du formulaire, ou bien ajouter du code de vérification dans la couche DAO avant de persister en base. Cette spécification a eu pour objectif, d’une part d’enrichir les entités métiers sur les valeurs que pouvaient prendre ses propriétés, et d’autre part de fournir un service capable de valider ces entités avec en plus un certain niveau d’information sur les cas non valides.

La JSR-303, finalisée en novembre 2009, fournit une standardisation de ces concepts et fait partie de Java EE 6. Emmanuel Bernard étant le spec lead de cette JSR, assez naturellement l’implémentation Hibernate Validator est devenue celle de référence. C’est cette dernière qui sera exclusivement évoquée dans cet article.

Concepts de base

On étudiera dans cet article la configuration par annotations, mais il reste possible de faire l’équivalent en fichier xml.

Contraintes

Une contrainte est une restriction sur un attribut d’un bean. Beaucoup sont déjà définies dans la JSR-303, Hibernate Validator en rajoute quelques-unes très utiles et l’utilisateur peut définir les siennes comme nous le verrons plus loin.

Voici un exemple pour valider qu’un attribut est non nul :

public class Personne{

    @NotNull
    private String nom;
    ...
}

Les contraintes peuvent être posées directement sur le champ, sur la propriété par l’intermédiaire de l’accesseur ou au niveau de la classe, ce qui nécessite de définir sa propre contrainte. La visibilité du champ n’a pas d’importance car il sera introspecté par réflexion mais il ne doit pas être statique.

Un graphe d’objets peut être validé dans son ensemble, ou bien plus finement. Si une entité à valider possède une autre entité ayant des contraintes, l’annotation @Valid permet de contraindre aussi la validation de cette entité. Cerise sur le gâteau, la validation sur une collection fonctionne aussi, tous les éléments de cette collection devant être valides.

Voici les contraintes standards:

contrainte signification types acceptés
@Null L’élément doit être nul Object
@NotNull L’élément doit être non nul Object
@AssertTrue L’élément doit être true boolean, Boolean
@AssertFalse L’élément doit être false boolean, Boolean
@Min L’élément doit être supérieur à la valeur spécifiée dans l’annotation BigDecimal, BigInteger, byte, short, int, long
@Max L’élément doit être inférieur à la valeur spécifiée dans l’annotation BigDecimal, BigInteger, byte, short, int, long
@DecimalMin L’élément doit être supérieur à la valeur spécifiée dans l’annotation BigDecimal, BigInteger, String, byte, short, int, long
@DecimalMax L’élément doit être inférieur à la valeur spécifiée dans l’annotation BigDecimal, BigInteger, String, byte, short, int, long
@Size L’élément doit être entre deux tailles spécifiées String, Collection, Map, Array
@Digits L’élément doit être un nombre compris dans une certaine fenêtre BigDecimal, BigInteger, String, byte, short, int, long
@Past L’élément doit être une date dans le passé Date, Calendar
@Future L’élément doit être une date dans le futur Date, Calendar
@Pattern L’élément doit respecter une expression régulière String

Hibernate Validator ajoute des contraintes intéressantes : @CreditCardNumber, @Email, @NotBlank, @NotEmpty, @Range, @ScriptAssert, @URL. @ScriptAssert est sans doute la plus complexe à mettre en place, elle s’appuie sur la JSR-223 qui fournit une API pour introduire les langages de script, plus de détails ici.

Service de validation

Pour valider ses entités il faut se munir tout naturellement d’un validateur, on passe pour cela par une factory fournie par l’API standard ( javax.validator ) :

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();

Validator validator = factory.getValidator();

Pour récupérer l’implémentation du validateur, l’API utilise le Service Provider de Java. Son fonctionnement est assez simple. Dans le jar contenant l’implémentation il faut ajouter dans le répertoire META-INF/services un fichier nommé javax.validation.spi.ValidationProvider qui contient le nom de la classe implémentant l’interface ValidationProvider. Pour Hibernate Validator il s’agit de la classe HibernateValidator.

Donc pour résumer, il n’y a pas de configuration à faire pour spécifier que Hibernate Validator fournira le Validator, il suffit juste d’ajouter le jar, sauf s’il existe d’autres implémentations de l’API de validation. Dans ce dernier cas, et si on veut être sûr d’utiliser Hibernate Validator, il est possible de définir le provider explicitement :

ValidatorFactory factory = Validation.byProvider(HibernateValidator.class).configure().buildValidatorFactory();

Ensuite on passe à la validation de nos entités :

Set<ConstraintViolation<Voiture>> constraintViolations = validator.validate(voiture);

On introduit ici l’objet ConstraintViolation, très riche en informations sur les raisons de la non validation d’un élément. C’est cet objet qui va par exemple nous permettre de nous fournir le message associé à l’erreur. Les méthodes de cette classe :

méthode description
getMessage() Récupère le message interpolé
getMessageTemplate() Récupère le message non-interpolé
getRootBean() L’objet racine non valide
getLeafBean() Dans le cas où l’objet à valider contient un autre objet qui se trouve être non valide, ce dernier sera retourné
getRootBeanClass() La classe de l’objet non valide
getPropertyPath() La propriété de l’entité qui n’est pas valide
getInvalidValue() La valeur erronée
getConstraintDescriptor() Un objet contenant des informations sur la contrainte elle-même

C’est l’occasion de remarquer que l’API de validation propose une gestion interne des ressources, on peut soit récupérer le message directement associé à une propriété ( getMessage() ) soit la clé ( getMessageTemplate() ). Dans le chapitre suivant on détaillera davantage la gestion des ressources.

On peut aussi avoir besoin de ne valider qu’une sous partie des contraintes d’une entité ou de rassembler plusieurs des contraintes de plusieurs entités. L’attribut groups des annotations de contraintes sert à ça. Elle contient une interface (ou une classe, ce qui est moins recommandé) qui sera commune aux contraintes:

public class Facture {
    @Past
    private Date dateCommande;

    @NotNull(groups=PaiementCheck.class)
    private String nomBanque;

    @CreditCardNumber(groups=PaiementCheck.class)
    private String creditCardNumber;
}

public interface PaiementCheck {}

...
    validator.validate(facture, PaiementCheck.class);
...

Gestion des ressources

A chaque type de contrainte est associé une clé, elle même liée à une propriété. Toutes les contraintes de base ont des messages déjà définis et Hibernate Validator propose même les traductions françaises. Ce message peut être surchargé directement au niveau de la contrainte:

@Min(message="ce champ doit être supérieur à {value}")

Grâce aux accolades, on peut accéder aux données de l’annotation (dans cet exemple à la valeur minimale). On peut échapper les accolades avec .

Les propriétés sont résolues par l’intermédiaire de l’interface MessageInterpolator qui, par défaut, va chercher un fichier ValidationMessages.properties à la racine du classpath. S’il n’en trouve pas à la racine il ira chercher celui dans le package org.hibernate.validator. On peut donc ainsi, facilement surcharger les propriétés de Hibernate Validator.

Il est possible d’utiliser ses propres resource bundles si on veut par exemple utiliser d’autres fichiers properties, ou les stocker ailleurs que dans le classpath. Pour cela il faut surcharger l’implémentation de ResourceBundleLocator. Ci-dessous, un exemple tiré de la documentation:

HibernateValidatorConfiguration configure = Validation.byProvider(HibernateValidator.class).configure();

ResourceBundleLocator defaultResourceBundleLocator = configure.getDefaultResourceBundleLocator();
ResourceBundleLocator myResourceBundleLocator = new MyCustomResourceBundleLocator(defaultResourceBundleLocator);

configure.messageInterpolator(new ResourceBundleMessageInterpolator(myResourceBundleLocator));

A un plus haut niveau, on peut également ne pas passer par des resource bundles mais implémenter sa propre stratégie pour, par exemple, accéder à une base de données. Pour cela il faut fournir une implémentation de l’interface MessageInterpolator.

Configuration<?> configuration = Validation.byDefaultProvider().configure();

ValidatorFactory factory = configuration
   .messageInterpolator(new MyMessageInterpolator(configuration.getDefaultMessageInterpolator()))
   .buildValidatorFactory();

Validator validator = factory.getValidator();

MyMessageInterpolator implémente donc MessageInterpolator qui a deux méthodes:

String interpolate(String messageTemplate, Context context);
String interpolate(String messageTemplate, Context context,  Locale locale);

messageTemplate étant la clé de la contrainte et context contient des informations sur cette contrainte. Libre ensuite au développeur d’associer la ressource correspondante comme il veut.

Définir sa propre contrainte

Il est possible de créer sa propre contrainte, pour cela on passe par 3 étapes:

  • créer l’annotation
  • implémenter le validateur
  • définir le message d’erreur

Nous allons faire un validateur pour les plaques d’immatriculation et vérifier qu’elles appartiennent à un département donné (il s’agira des anciennes normes, par exemple 123-ABC-93, pas des nouvelles normes européennes). Commençons par l’annotation :

@Target({METHOD,FIELD,ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy=ImmaticulationValidator.class)
@Documented
public @interface CheckImmatriculation {

   String message() default "{fr.xebia.constraints.checkimmatriculation}";

   Class<?>[] groups() default {};

   Class<? extends Payload>[] payload() default {};

   String departement();
}

On voit apparaître la notion de Payload, elle peut être utilisée pour enrichir les informations sur cette contrainte. L’exemple proposé en général est le cas où on veut ajouter un niveau de sévérité à une contrainte. On peut récupérer la classe associée dans la description de la contrainte : ConstraintViolation.getConstraintDescriptor().getPayload().

On écrit ensuite le validateur associé :

public class ImmaticulationValidator implements
       ConstraintValidator<CheckImmatriculation, String> {

   private String departement;

   public void initialize(CheckImmatriculation constraintAnnotation) {
       departement = constraintAnnotation.departement();
   }

   public boolean isValid(String value, ConstraintValidatorContext context) {
       if (!Pattern.matches("
d{1,3}-[a-zA-Z]{2,3}-
d{2}", value)) {
           return false;
       }
       // On récupère le département donné
       String d = value.substring(value.length() - 2);
       return departement.equals(d);
   }

}

Enfin on doit ajouter le message qui sera retourné en cas d’erreur dans le fichier de properties ValidationMessages.properties comme vu précédemment. On peut par exemple afficher le département attendu:

fr.xebia.constraints.checkimmatriculation=Le département doit être {departement}.

Spring/SpringMVC

Depuis la version 3.0 de Spring MVC la JSR-303 est complètement supportée. Tout est donc grandement facilité et valider un formulaire devient quasiment immédiat. On peut résumer la procédure à l’ajout de l’annotation @Valid dans la requête POST de soumission du formulaire:

public void submitVoiture(@Valid VoitureForm voiture, , Errors errors){
     logger.info("Nombre d'erreur"+errors.getErrorCount());
     ...
}

Les contraintes non validées seront automatiquement injectées dans errors et donc accessibles dans la vue :

<form:form action="edit" modelAndAttribute="voiture">
    <form:errors path="*"/>
    <form:input path="immatriculation"/>
    ...
    <input type="submit"/>
</form:form>

Outre Spring MVC, de plus en plus de frameworks de présentation adoptent la JSR-303, remplaçant leurs anciens codes spécifiques par ce nouveau standard. Naturellement JSF 2.0 l’utilise mais on peut aussi citer Wicket ou Tapestry.

Hibernate

Nous avons vu qu’il était possible de valider programmatiquement une entité. Néanmoins, on peut vouloir être encore plus prudent et empêcher les opérations de persistance si une contrainte est violée. Hibernate propose un mécanisme de listeners sur les événements insert, update et remove. Voici un exemple de configuration possible:

<persistence ...>
  <persistence-unit ...>
    ...
    <properties>
      <property name="javax.persistence.validation.mode"
                value="callback, ddl"/>
    </properties>
  </persistence-unit>
</persistence>

Il existe plusieurs modes: none, auto (s’il n’y a aucun jar de validation, il n’y aura pas de validation), callback (s’il n’y a aucun jar une exception est lancée au démarrage), ddl. Cette dernière permet de générer le schéma de base avec les contraintes définies dans l’entité.

Il est possible de différencier les comportements selon le type d’opération:

<persistence ...>
  <persistence-unit ...>
    ...
    <properties>
      <property name="javax.persistence.validation.group.pre-update"
                value="javax.validation.group.Default, com.acme.group.Strict"/>
      <property name="javax.persistence.validation.group.pre-remove"
                value="com.acme.group.OnDelete"/>
      <property name="org.hibernate.validator.group.ddl"
                value="com.acme.group.DDL"/>
    </properties>
  </persistence-unit>
</persistence>

En cas de violation d’une contrainte une exception de type ConstraintViolationException est lancée, éventuellement englobée dans une exception RollbackException en mode transactionnel. Cette exception contient la liste des ConstraintViolation de l’API Bean Validation vue plus haut.

Une fonctionnalité de l’API encore non évoquée, mais qui rejoint les problématiques souvent rencontrées dans Hibernate, est la notion de traversabilité, formalisée par l’interface TraversableResolver. Celle-ci contient deux méthodes isReachable et isCascadable qui peuvent permettre dans certains cas de désactiver une validation d’un sous-objet. Cela peut permettre lorsqu’on manipule des objets en lazy loading, de préserver ce non-chargement.

Conclusion

Bean Validation impressionne par sa vision complète des problématiques de validation, peu de cas semblent avoir été ignorés ou oubliés, et dans le cas contraire l’API est suffisamment souple pour pouvoir être étendue proprement. Certains regretteront peut-être qu’il n’y ait pas plus de contraintes pré-définies, mais il est extrêmement difficile de définir des contraintes universelles et ajouter sa propre contrainte est vraiment facile.

11 Responses

  • Bonjour Guillaume,

    Merci pour l’article. L’intégration à Spring est intéressante.
    J’aimerais juste apporter un éclairage sur l’utilisation de la jsr-303.

    La validation fournie par la jsr-303 est une validation de type « validation donnée ».
    Elle est effectivement intéressante pour les « invariants ». Les invariants sont des données qui ont la même validation quel que soit le use-case.
    Exemple : donnée obligatoire, format de sécurité sociale, etc.
    On ne pourra pas valider de telles contraintes :
    – BR-127 : La raison d’une annulation est obligatoire
    – BR-542 : Le nombre maximal d’alertes est de 3 tant que la transaction est à l’état « non payée ».

    La 1ère règle montre que le champ « commentaire » est non obligatoire d’habitude mais est obligatoire dans le cas d’une annulation. Dans ce cas il ne faut pas annoter le champ @NotNull.
    La 2ème règle montre bien que l’on ne peut pas annoter @Valid sur le collaborateur Transaction, ni annoter @Size(min = 0, max = 3) sur le collaborateur Set. Ca n’est pas vrai dans tous les cas.

    La solution est d’utiliser la jsr-303 pour les invariants de format (donnée correctement formatée, périmètre de valeur correct, type de valeur correct, caractère obligatoire permanent).
    Pour une gestion des données qui dépendent du use-case, utiliser un validateur.
    Le concept est celui des validateurs spring mais sans être liée à l’API d’erreur qui est lourde d’après moi. On prendra soin d’invoquer le validateur de la jsr-303 (pour appliquer la validation données) avant de commencer notre validation métier.

    Cdt,

    Louis.

  • Excellente piqure de rappel pour certains et bonne entrée en matière pour d’autres, cet article se laisse lire et à le mérite d’être en français. Bravo !

    J’ai une petite question concernant le javax.persistence.validation.mode: Si on ne configure rien, par défaut cette propriété est en auto c’est ca ? (si j’en crois la doc jboss)

    Pour quand on ne met rien, on ne retrouve pas les contraintes en bdd dans le schéma.

    Une idée ?

  • @louis

    Merci pour ce retour d’expèrience concret.

    Il me semble que dans tes deux cas on peut tout de même utiliser la jsr-303 en s’appuyant sur la notion de groupe ( http://people.redhat.com/~ebernard/validation/#validationapi-validatorapi-groups ).

    Par exemple pour le cas de l’annulation, on créerait une interface AnnulationCheck. L’annotation serait donc @NotNull(groups={AnnulationCheck.class}). Ensuite pour la validation en cas d’annulation validator.validate(monObjet, AnnulationCheck.class). Dans les autres validations, si on ne précise pas le groupe, la validation sera désactivée sur le champ commentaire.

    L’autre exemple semble plus complexe mais je pense que cette solution doit fonctionner aussi.

  • @Cyril

    D’après la doc hibernate ( http://docs.jboss.org/hibernate/annotations/3.5/reference/en/html/additionalmodules.html#d0e3875 ), il suffit effectivement d’ajouter dans le classpath un jar implémentant la jsr-303. Si on veut forcer la présence de ce jar il faut passer en mode callback et si au contraire on ne veut pas du tout de validation il faut le mode none.

    J’espère que ça répond à ta question.

  • Merci pour le retour Guillaume!

    Effectivement c’est possible avec les groupes. Mais moi j’aime la simplicité :). Lorsqe ça devient trop fastidieux de définir un ensemble de contraintes j’aime autant encapsuler ma validation dans une classe que j’appelle.

    Je vais explorer les groupes plus en profondeur mais par principe je préfère encapsuler mes validations dans une classe (ça me permet d’être fortement cohésif) dont la responsabilité est de valider un Use Case complexe qui implique plusieurs objets qui collaborent entre eux. C’est plus cohésif mais très certainement moins industrialisable pour les outils de génération.

    Je préfère une classe de type RenewXXXValidator qui va faire des contrôles de type business dans les objets metiers User, Transaction, Advert, Media
    plutôt que de définir un groupe @RenewXXX sur les propriétés de chacun des objets ci-dessus qui participent au renouvellement de XXX.

    Je suppose que je me fais vieux et que je devrais me mettre aux nouveaux outils :).

    Bonne soirée.

  • Bonjour

    Sympa cette présentation détaillée.

    Pour avoir mis en pratique Bean Validation depuis sa sortie, voici quelques retours d’expériences:
    - intégration avec Wicket est sympa (cf http://mvnrepository.com/artifact/org.wicketstuff/jsr303), surtout le PropertyValidator (à condition d’utiliser des PropertyModel partout, rien n’est parfait)
    - principe fort intéressant: les entités définissent désormais le « socle vital » de règles de validations (ce qui est nommé les invariants plus haut). Avoir tout cela en un seul endroit et repris partout ailleurs est vraiment intéressant.
    - réalisation de l’API pas tip top : l’implémentation de l’internationalisation avait un bug plus que surprenant. Plus embêtant, l’implémentation est verbeuse et ne prend pas en compte les génériques, par exemple un Set.

    au final, on est mieux avec que sans, mais l’API aurait sans doute pu être plus compact et proche de Java6.

    ++
    joseph

  • @Louis

    S’il n’y a pas de besoins particuliers pour migrer vers un framework standard et que les services de validations « manuels » sont satisfaisants, autant rester sur un existant qu’on maîtrise bien.

    Pour de nouveaux projets par contre je trouve ça intéressant de pouvoir s’appuyer sur un socle commun et pouvoir profiter de l’expérience d’autres projets.

  • @Joseph

    merci pour ce retour d’expérience.

    Lorsque vous parlez de « réalisation de l’API pas tip top », il s’agit d’Hibernate Validator ?

  • @Arnaud

    Hum, un peu des deux ;)

    En fait, le bug concernant l’internationalisation et la mise en œuvre de sa correction sont, AMHA, spécifiques à Hibernate Validator 4.x.

    La verbosité de l’API et la non prise en compte des génériques sont eux plus à lier à la JSR303 elle même.

    Au demeurant, ces 2 derniers points ont certainement fait l’objet de discussion pendant l’élaboration de la JSR et, assurément, ne sont pas d’une résolution aisée. Mais le résultat reste verbeux et surprenant à l’heure de Java 7.

    ++
    joseph

  • Hi,

    GraniteDS 2.2 comes with an ActionScript3 implementation of the Bean Validation specification and its code generation tools may be configured to automatically replicate Java bean constraint annotations into the generated ActionScript3 model.

    FW.

  • Forgot the links for the GraniteDS validation framework documentation: http://www.graniteds.org/confluence/display/DOC22/3.+Validation+Framework+%28JSR-303+like%29.

    FW.

Laisser un commentaire