JSR 303 (Bean Validation) : état des lieux

le 07/09/2011 par Ahmed Mseddi
Tags: Software Engineering

La JSR 303 (Java Specification Request) a été lancée en 2006. Elle a pour objet d'éviter la duplication de la validation des données dans les diverses couches de l'application en la localisant dans la définition des Beans Java. Ceci, dans le but de gagner en productivité et d'éviter les bugs liés à la redondance de la validation. 5 ans après son lancement, nous sommes tentés d'en savoir plus sur le chemin parcouru par cette JSR et surtout de savoir si oui ou non elle a atteint ses objectifs !

Avant toute chose cependant, il est primordial de se poser quelques questions basiques qui nous permettront de comprendre cette JSR. En effet, quels sont les principes de cette JSR ? Quelles sont les différentes implémentations qui en ont été faites ? Sont-elles au même degré de maturité ? Cette JSR s’intègre-t-elle avec les frameworks existants ? Ou se situe-t-elle par rapport aux autres outils de validation ?

La JSR 303 par l'exemple

Dans cette partie, après une courte introduction décrivant succinctement la JSR, je présenterai deux exemples : le premier utilise les annotations définies dans la JSR et le deuxième montre comment créer nos propres annotations pour valider une classe Utilisateur. Mais avant toute chose, je vous propose une brève présentation de la JSR.

Présentation de la JSR

La JSR 303 définit un modèle de meta-données et une API pour valider les Beans Java. Cette validation s'effectue en utilisant les annotations mais il est possible d'utiliser des fichiers XML. Actuellement, cette JSR a passé toutes les étapes (stages) puisqu'elle en est à la dernière, i.e. celle de Final Release depuis le 16 novembre 2009. Elle était sous la direction d'Emmanuel Bernard, lead développeur chez JBoss, une division de Red Hat.

1er exemple : utilisation des annotations

Tout d'abord voici la classe Utilisateur que je vais utiliser pour illustrer mes exemples :

public class Utilisateur {

    private String login;
    private String mdp;
    private Date dateDeNaissance;

    //Constructeurs, getters et setters
}

Pour notre classe Utilisateur, nous avons plusieurs contraintes :

  • Le login ne doit pas être vide
  • Le mot de passe doit contenir entre 8 et 16 caractères
  • La date de naissance doit se situer dans le passé

Nous allons donc annoter les attributs de notre classe pour prendre en compte ces contraintes :

public class Utilisateur {

    @NotNull
    private String login;

    @Size(min = 8, max = 16)
    private String mdp;

    @Past
    private Date dateDeNaissance;

    //Constructeurs, getters et setters
}

Pour illuster cet exemple, on définit une classe Main dans laquelle on instancie un nouvel utilisateur ayant des attributs qui ne respectent pas les contraintes :

public class Main {

	public static void main(String[] args) throws ParseException {

		Utilisateur user = new Utilisateur();

		long dayInMillis = 24*60*60*1000;

		user.setLogin(null);
		user.setMdp("mdp");
		user.setDateDeNaissance(new Date(System.currentTimeMillis() + dayInMillis ));

		Validator validator = Validation
				.buildDefaultValidatorFactory().getValidator();

		Set<ConstraintViolation<Utilisateur>> violations = validator
				.validate(user);

		System.out.println("Nombre de violations : " + violations.size());

		for (ConstraintViolation
				constraintViolation : violations) {

			System.out.println("Valeur '"+
					constraintViolation.getInvalidValue() + 
					"' incorrecte pour '"+ 
					constraintViolation.getPropertyPath() + 
					"' : " +
					constraintViolation.getMessage());
		}
	}
}

En exécutant le code précédent nous obtenons les sorties suivantes dans la console :

Nombre de violations : 3
Valeur 'Thu Aug 18 12:13:56 CEST 2011' incorrecte pour 'dateDeNaissance' : doit être dans le passé
Valeur 'mdp' incorrecte pour 'mdp' : la taille doit être entre 8 et 16
Valeur 'null' incorrecte pour 'login' : ne peut pas être nul

On voit donc qu'on ne respecte pas les trois contraintes et qu'on a à chaque fois un message explicatif.

Notons qu'on aurait tout aussi bien pu définir les contraintes dans un fichier XML constraint-utilisateur.xml :

<constraint-mappings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                     xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/mapping validation-mapping-1.0.xsd"
                     xmlns="http://jboss.org/xml/ns/javax/validation/mapping">
    <default-package>com.octo.jsr303</default-package>
    <bean class="Utilisateur" ignore-annotations="true">
        <field name="login">
            <constraint annotation="javax.validation.constraints.NotNull"/>
        </field>
        <field name="mdp">
            <constraint annotation="javax.validation.constraints.Size">
                <element name="min">8</element>
                <element name="max">16</element>
            </constraint>
        </field>
        <field name="dateDeNaissance">
        	<constraint annotation="javax.validation.constraints.Past"/>
        </field>
    </bean>
</constraint-mappings>

Il aurait ensuite été nécessaire de déclarer ce fichier dans un fichier validation.xml pour qu'il soit pris en compte. La documentation détaillée de l'utilisation de la validation XML est disponible ici.

2ème exemple : Création d'une annotation

Il est parfois nécessaire de créer ses propres annotations lorsque celles définies par défaut ne répondent pas aux besoins. Imaginons par exemple qu'on veuille ajouter aux utilisateurs l'attribut article qui représente un lien vers l'article favori de l'utilisateur parmi les articles du blog Octo.

public class Utilisateur {

	@NotNull
	private String login;

	@Size(min = 8, max = 16)
	private String mdp;

	@Past
	private Date dateDeNaissance;

	@OctoBlog
	private String article;

	// Constructeurs, getters et setters
}

On commence donc par définir une nouvelle annotation @OctoBlog :

@Constraint(validatedBy = OctoBlogValidator.class)
@Target(value = ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OctoBlog {

	String message() default "L'article n'appartient pas au blog octo";

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

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

Pour cette interface il est obligatoire de de redéfinir messages(), groups() et payload. Il est aussi nécessaire de définir l'annotation @Retention au RUNTIME pour que la validation s'effectue. L'annotation @Target sert à dire quel type d'élément sera validé et @Constraint la classe qui validera cet élément.

Ci-dessous la classe de validation :

public class OctoBlogValidator implements
		ConstraintValidator<OctoBlog, String> {

	@Override
	public void initialize(OctoBlog constraintAnnotation) {

	}

	@Override
	public boolean isValid(String value,
			ConstraintValidatorContext context) {
		return value.startsWith("https://blog.octo.com/");
	}
}

Pour illustrer ce deuxième exemple, on exécute le main suivant :

public class Main {

	public static void main(String[] args) throws ParseException {

		Utilisateur user = new Utilisateur();
		Validator validator = Validation
				.buildDefaultValidatorFactory().getValidator();

		user.setLogin("ams");
		user.setMdp("motDePasse");
		user.setDateDeNaissance(new Date(System.currentTimeMillis()));
		user.setArticle("http://www.blog.com/mon-article");

		System.out.println("Première validation : ");
		validateUser(user, validator);

		user.setArticle("https://blog.octo.com/jsr-303-bean-validation-etat-des-lieux");

		System.out.println("Deuxième validation : ");
		validateUser(user, validator);

	}

	private static void validateUser(Utilisateur utilisateur,
			Validator validator) {
		Set<ConstraintViolation<Utilisateur>> violations;
		violations = validator.validate(utilisateur);

		System.out.println("Nombre de violations : " + violations.size());

		for (ConstraintViolation constraintViolation : violations) {

			System.out.println("Valeur '"
					+ constraintViolation.getInvalidValue()
					+ "' incorrecte pour '"
					+ constraintViolation.getPropertyPath() + "' : "
					+ constraintViolation.getMessage());
		}
	}
}

Ce qui nous donne la sortie suivante dans la console :

Première validation : 
Nombre de violations : 1
Valeur 'http://www.blog.com/mon-article' incorrecte pour 'article' : L'article n'appartient pas au blog octo
Deuxième validation : 
Nombre de violations : 0

Notons qu'il nous était possible d'utiliser l'annotation @Pattern de la manière suivante :

@Pattern(regexp = "http://blog\\.octo\\.com/.*",
			message = "L'article n'appartient pas au blog octo")
	private String article;

Une troisième façon de faire aurait été d'ajouter @Pattern en annotation de @OctoBlog :

@Constraint(validatedBy = OctoBlogValidator.class)
@Target(value = ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Pattern(regexp = "http://blog\\.octo\\.com/.*")
@ReportAsSingleViolation
public @interface OctoBlog {

	String message() default "L'article n'appartient pas au blog octo";

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

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

L'annotation @ReportAsSingleViolation indique qu'il faut ignorer les messages d'erreur des annotations et qu'il faut utiliser le message défini dans @OctoBlog.

Implémentation de référence : Hibernate Validator

Pour exécuter les exemples précédents j'ai utilisé la version 4.2.0.Final (disponible depuis juin 2011) d'Hibernate Validator, implémentation de référence de la JSR-303. En plus d'implémenter les annotations définies par la JSR, Hibernate Validator en ajoute des nouvelles comme @Email, @NotEmpty et @CreditCardNumber.

Autre implémentation : Apache Bean Validation

Anciennement appelé agimatec-validation, ce projet est depuis mars 2010 en incubation chez apache : https://cwiki.apache.org/BeanValidation/ et la dernière version publiée est la 0.3-incubating.

Pour tester cette implémentation, j'ai modifié les imports dans les exemples de la partie précédente. Le comportement obtenu était semblable à l'exception des messages d'erreur qui étaient en anglais. Bon point donc pour Hibernate qui a internationalisé les messages.

D'une manière générale, si vous voulez utiliser la JSR-303 pour valider les Beans dans vos projets, la préconisation est d'utiliser Hibernate Validator. En effet, cette implémentation est stable et les nouvelles releases sont assez régulières. Pour ceux qui veulent utiliser Apache Bean Validation, il est conseillé d'attendre que ce projet passe l'étape d'incubation.

Certains projets sont plutôt hésitants quant à l'utilisation de la validation au niveau des Beans Java craignant une baisse des performances générales. Pour avoir une idée du temps que prend la validation des objets, je vous conseille de regarder le benchmark publié ici qui compare les temps nécessaires pour valider les Beans en utilisant les deux implémentations. Ce qui ressort de cette étude est que Apache Bean Validation 0.1-incubating est plus performant que Hibernate Validator 4.1.0.CR1. Cependant ce benchmark a été réalisé en 2010 et depuis la version 4.2.0.Final de Hiberante Validator est sortie, il serait donc intéressant de refaire les tests car cette dernière version est sensée améliorer les performances en diminuant les temps de validation.

GWT et la JSR-303

Le framework gwt-validation implémente  la JSR 303 et propose de valider les Beans aussi bien côté client que côté serveur. D'après le wiki du projet l'implémentation de la JSR est finie à 80%. Mais il ne sera bientôt plus obligatoire de passer par ce framework pour utiliser la JSR-303. En effet, depuis peu, Google a commencé à intégrer cette JSR dans GWT : http://code.google.com/p/google-web-toolkit/wiki/BeanValidation. Cette intégration se base sur l'implémentation de référence Hibernate Validator et propose aussi une validation côtés serveur et client. Cette intégration n'est pas disponible dans GWT 2.3 mais l'est dans le trunk. Pour ceux qui veulent tester, un exemple d'utilisation est disponible ici. Il faut cependant faire attention car, comme annoncé sur la page du wiki du projet, la validation n'est pas encore mature et l'API peut encore beaucoup évoluer.

Conclusion : JSR-303 Vs le reste du monde

Le besoin de valider les Beans Java est bien plus antérieur à la parution de JSR-303. En effet, d'autres frameworks se proposaient déjà de valider les objets comme Apache Commons Validator dont la première release date de 2002. D'autres  frameworks Java embarquent leur propre mécanisme de validation comme Spring avec son Validator. Pour utiliser ce dernier afin de valider notre classe Utilisateur, il aurait fallu créer une deuxième classe UtilisateurValidator qui implémente l'interface Validator de Spring.

Ces frameworks ayant été pensés à l'ère pré-JDK5, la JSR-303 simplifie la validation des Beans grâce à l'utilisation des annotations et rend le code plus lisible quant aux contraintes qu'il doit respecter. Elle apporte aussi un standard à cette partie très importante des projets informatiques que représente la validation des données.