Un petit billet sur la JSR 303, que j’avais commencé il y a plus d’un mois. Tout d’abord en quelques mots, la JSR 303 propose de standardiser en Java l’expression des contraintes du monde objet ainsi qu’un moteur de validation. En effet nous vivons dans un monde de contraintes dès que nous commençons à programmer. Sur une classe Personne, l’attribut âge ne peut être qu’un entier positif par exemple. Dans le monde de la finance, des règles bien plus complexes sont aussi implicites pour les personnes qui manipulent des concepts financiers. La validation de ce que peut saisir l’utilisateur peut constituer une grosse partie métier d’une application.
Surgit un débat : où peut s’effectuer la validation par exemple dans une application web ? Dans la couche de présentation ? Dans la couche de service ? Au niveau du moteur de mapping objet-relationnel ? Ou dans la base de données ?
La JSR 303 propose 3 domaines dans sa solution :
– Comment exprimer une contrainte ?
– comment valider un objet ?
– comment retrouver et utiliser les contraintes définies sur un objet ?
Dis papa c’est quoi cette petite contrainte ?
Pour moi une contrainte c’est quelque chose en Java que le type d’attribut ne contraint pas assez. Dans une classe Java, un attribut de type Integer au lieu de String nous donne déjà une idée sur ce que l’on veut stocker. Le problème est que ce n’est pas suffisant pour donner de la consistance.
Prenons une classe Utilisateur avec un attribut email qui représente une adresse email.
/** * User pour tester la JSR 303... * Created by IntelliJ IDEA. * User: nicolas.martignole * Date: 05-Jun-2008 * Time: 17:26:57 */ public class Utilisateur { private String email; public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
Je ne souhaite pas stocker un email qui serait null ou une chaine de caractères de plus de 50 caractères. Pour cela je peux modifier mon bean et faire en sorte que la méthode setEmail jette une exception lorsque l’argument passé n’est pas correct :
public void setEmail(String email) { if (email == null) throw new IllegalArgumentException("Email ne doit pas être null"); if (email.length() > 50) throw new IllegalArgumentException("Le champ email ne doit pas faire plus de 50 caractères"); this.email = email; }
Jusqu’ici cela fonctionne très bien. Les remarques sont que ma contrainte ne saute pas aux yeux, sauf à lire le code. Et en plus je ne peux pas l’introspecter et l’extraire du code pour m’en servir dans mon application par exemple.
Le type de message pour l’utilisateur ici est un peu violent. Une exception Java pour du code métier… Je ne suis pas très sympathique pour ceux qui se serviront de mon bean non ?
Est-ce qu’il faut exprimer la contrainte dans le modèle ?
Après tout je pourrais avoir le droit d’avoir une class java Utilisateur sans contraintes sur l’attribut email, mais une règle dans ma couche de présentation ou de service pour restreindre la puissance non ?
Si mon Utilisateur était un Entity bean, persisté vers une base, après tout je pourrais utiliser les types de colonnes dans ma base afin d’empêcher de stocker une adresse email null et de plus de 50 caractères en me basant sur des contraintes dans la base. Vous pensez pas que c’est un peu trop bas vous ?
CREATE TABLE UTILISATEUR { id bigint not null primary key, email varchar(50) not null }
Je peux aussi dans ma couche de présentation ajouter une contrainte afin de tester les arguments saisis par l’utilisateur avant d’appeler ma couche de service. Ou dans ma partie métier, je peux encore exprimer une contrainte…
Bref le souci c’est qu’à en mettre partout, surtout entre la base et le modèle, on finit par ne pas voir exactement les contraintes sur les objets. En plus le souci est que l’on aura un empilement de contraintes les unes après les autres. C’est parfois souhaitable dans certaines architectures cependant.
Le meilleur endroit pour stocker les contraintes, tel qu’il est proposé par la JSR 303 est dans le bean lui-même. Donc au niveau du modèle.
La JSR 303 propose d’utiliser les annotations introduites en Java 5 pour tout simplement exprimer les contraintes sur le bean. Donc ma class User deviendra tout simplement :
/** * Petit classe pour demontrer le principe de la JSR 303. * * Created by IntelliJ IDEA the best IDE in the world. Eclipse sucks. * User: nicolas.martignole * Date: 05-Jun-2008 * Time: 17:48:09 */ public class Utilisateur { @NotNull(message = "Le champ email ne peut pas etre null") @Email(message = "Merci d'entrer un email valide") @Length(max = 50, message = "Pas plus de {max} caracteres svp") private String email; public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
On constate que l’on peut maintenant indiquer que l’attribut email ne peut pas être null, doit être une adresse de courrier électronique valide et d’une longueur maximum de 50 caractères.
La JSR 303 permet d’annoter une class complète (Utilisateur) un attribut (email) mais pas encore les arguments passés à une méthode.
Vous pouvez créer vos propres contraintes et utiliser aussi des expressions régulières pour exprimer une contrainte. Je ne suis pas très inspiré donc disons qu’une méthode setAge prend en argument une String. Cette String doit être de type numérique, sur 2 digits. Par exemple « 03 » ou « 43 ». Mais pas « 2 » ou « toto »
L’annotation Pattern vous permet de déclarer votre pattern de validation :
@Pattern(regex = "\\d+\\d+") @NotNull public int parseAge(String age) { return Integer.parseInt(age); }
Les contraintes peuvent être déclarées dans des classes abstraites, donc héritées par des classes filles. Lorsqu’une classe fille surcharge une méthode d’une super classe qui a une contrainte, la règle est additive sauf si vous redéfinissez la même contrainte. Je m’explique. Si dans une class « PetitUtilisateur » je surcharge à nouveau la méthode parseAge je peux tout à fait changer ma contrainte pour qu’elle soit moins restrictive.
Si nous souhaitons maintenant définir une contrainte pour qu’un numéro de carte bleue soit valide, il est possible de se lancer dans l’écriture de sa propre contrainte. Pour faire simple disons que l’on souhaite vérifier que le numéro comporte 16 digits et commence par 4973 (c’est le cas de la Société Générale).
Dans un premier temps on va déclarer une nouvelle annotation comme d’habitude en Java 5
@java.lang.annotation.Documented @ConstraintValidator(value = CarteBleueValidator.class) @java.lang.annotation.Target(value = {java.lang.annotation.ElementType.FIELD}) @java.lang.annotation.Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME) public @interface CarteBleue { String message() default ""; String[] groups() default {}; String bankName() default ""; }
ConstraintValidator permet de déclarer la classe Java à instancier afin d’effectuer la validation. Je déclare ma contrainte comme étant valide pour un champ (FIELD) et aussi comme étant retenue à l’exécution. Vous êtes obligés par contre d’utiliser la retention policy RUNTIME pour que la validation soit effective. Il faut aussi que les attributs message et groups soient déclarés. Message permet d’indiquer un message d’erreur par défaut et groups est utilisé pour les contraintes cascadées.
Voyons maintenant comment implémenter CarteBleueValidator pour donc effectuer une validation de nos numéros de carte bleue :
Attention : je n’ai pas testé le code suivant. Je l’ai écrit à la lecture de différents articles publiés par Emmanuel Bernard et sur les présentations de Java One 2008. A tester donc
/** * Validator for CarteBleue * Created by IntelliJ IDEA. No you really use Eclipse ? boooh. * User: nicolas.martignole * Date: 05-Jun-2008 * Time: 18:14:50 */ public class CarteBleueValidator implements Constraint{ private String bankName; public void initialize(CarteBleue annotation) { bankName=annotation.bankName(); } public boolean isValid(Object value) { if (value == null) return true; if (!(value instanceof Double)) throw new IllegalArgumentException("@CarteBleue only applies to Double"); Double cbNumber = (Double) value; // Si moins de 16 digits alors le numero n’est pas complet if(cbNumber < Double.parseDouble("1000000000000000")) { // question : comment positionner un message d’erreur ? pas possible ici // car nous traitons un NotValid. return false; } if(bankName==null){ // pas de banque dont on retourne true. Le numero est bon return true; } // Dans l’annotation on a mis bankName = “SG” if(bankName.equalsIgnoreCase("SG")){ if(cbNumber.toString().startsWith("4973")){ return true; } return false; } return true; } }
Une fois tout ceci écrit je peux alors ajouter un attribut Double dans ma classe Utilisateur et l’annoter avec mon Validateur :
@CarteBleue(bankName = "SG") private Double creditCardNumber;
Comme vous pouvez l’imaginer il existe un certain nombre de validateurs par défaut afin de vous éviter de réécrire la même logique plusieurs fois.
Vous pouvez déclarer :
<br />
@NotNull, @AssertTrue, @AssertFalse et @NotEmpty pour des tests simples.<br />
@Length, @Size pour les chaines, les collections et les tableaux<br />
@Min et @Max pour les Number.<br />
@Email par exemple <br />
@Pattern pour utiliser une expression régulière<br />
@Valid pour simplement valider un objet ou un attribut.<br />
La validation peut aussi maintenant être appelée par votre code métier, comme c’est le cas dans JBoss Seam qui utilise le moteur de validation d’Hibernate.
L’équipe de la JSR 303 travaille maintenant sur des problèmes plus pointus comme le support de l’internationalisation et sur le support de la validation des paramètres d’une méthode, ce qui n’est pas encore possible pour l’instant.
Je me pose pas mal de questions sur justement tout ce qui tourne autour de l’i18n. Prenons un attribut de type Date appelé « dateDeNaissance ». Vous serez d’accord avec moi pour dire que lorsque l’utilisateur effectue la saisie de la date de naissance dans la couche de présentation, si l’on veut valider cette date il va falloir implémenter des Validators en mesure de détecter la Locale de l’utilisateur. Moi en tant que français je vais utiliser jj/mm/aa. Un anglais par exemple va saisir mm/jj/aa. La question que je me pose : si le Validator se base sur la Locale courante de Java, qui est en fait celle du serveur d’application par exemple, celle-ci peut être différente de la Locale de l’utilisateur… Bref notre beau système de validation ne marchera pas dans ce cas…
Pour conclure je sais que je n’ai pas couvert toute la spéc JSR 303 et que le sujet est encore vaste. Il y a aussi des débats sur le risque de se dire que trop d’annotations tue l’annotation… J’en discutais avec Florent Ramière lundi dernier.
Ne risque pas-t-on de se retrouver avec des classes hyper annotées avec peu de code ?
Flavien (un autre lecteur assidu) va me demander encore si au niveau performance, cela ne coûte pas plus cher que de coder à l’ancienne. S’agissant d’annotation de type RUNTIME la réponse est non à priori. Il y a un peu d’introspection à l’exécution pour instancier le bon bean de validation, puis ensuite le reste ne doit pas coûter trop cher. D’ailleurs la méthode isValid du validateur doit être implémentée pour supporter des appels concurrents. Vous voyez ce que cela veut dire ?
Sur ce je vous souhaite une bonne soirée.
Mots clés : jsr303, jsr-303, jsr 303
0 no like
et les annotations peuvent cohabiter avec JPA ?
je pense à un bean metier persistant auquel je voudrais déclarer des contraintes .