Validation par annotations dans Spring

Utiliser les annotations pour vérifier les données dans Spring

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

Dans l'article précédent on a découvert la création des formulaires sous Spring. L'élément indissociable de cette thématique est la validation. C'est elle qui garantit la cohérence des données et une meilleure sécurité de notre application.

La validation mise à disposition dans Spring est applicable dans toutes les couches de l'application. Elle peut donc aussi bien être associée à la couche des services que celle des contrôleurs. Dans notre système d'exemple on utilisera cette deuxième solution. On verra plus loin qu'on peut aborder la validation selon deux manières différentes. La première, plus automatique, repose à la validation par annotations spécifiées dans les fichiers des JavaBeans passés dans la requête. L'autre, plus manuelle, consiste à écrire un validateur.

Les données validées ne doivent pas provenir de la requête. Dans certains cas (validation manuelle), elles peuvent être saisies directement dans le code, avant l'appel du validateur.

Validation par annotations dans Spring

Avant on a découvert que pour transformer un JavaBean en objet transférable via une requête HTTP, on doit l'annoter avec @ModelAttribute. Pour le marquer en tant qu'un objet soumis à la validation, on peut utiliser une des deux annotations : @Valid et @Validated. Quelle est la différence entre les deux ? La première fait partie du package javax.validation. L'autre est issue du package org.springframework.validation.annotation. Elle n'est donc pas un standard dans le monde des applications Java. Une autre différence, plus importante, réside dans les fonctionnalités. @Valid permet de créer un seul modèle de validation par objet tandis que @Validated permet de créer des groupes de validation. On peut constater cette différence dans ces deux exemples qu'on décrira plus loin dans l'article :

// @Valid case
@RequestMapping("/suggestions")
@Controller
public class SuggestionController extends FrontendController {
    @RequestMapping(value = "/new", method = RequestMethod.POST)
    public String handleNewSuggestion(@ModelAttribute("suggestion") @Valid Suggestion suggestion, BindingResult binRes, @LoggedUser AuthenticationFrontendUserDetails user, Model layout, RedirectAttributes redAtt) {
        
        if(binRes.hasErrors()) {
            // ...
        }
    }
}

// @Validated case
@Controller
public class SubscriberController extends FrontendController  {
    @RequestMapping(value = "/register" ,method = RequestMethod.POST)
    public String registerHandle(@ModelAttribute("subscriber") @Validated({SubscriberRegisterCheck.class}) Subscriber subscriber, BindingResult binRes, RedirectAttributes redAtt) {
        logger.info("Received POST request " + subscriber);
        if(binRes.hasErrors()) {
            // ...
        }
    }
}

La différence entre ces deux méthodes est la présence d'un groupe de validation dans le cas de @Validated. Il est important de souligner une chose à ce niveau. Le paramètre BindingResult binRes doit se trouver directement après le modèle à valider. Dans le cas contraire on peut s'attendre à une exception.

Regardons maintenant les entités que valide le code ci-dessus. Tout d'abord l'entité Suggestion qui se base sur l'annotation classique de validation :

@Entity
@Table(name = "suggestion")
public class Suggestion  implements Serializable {
    // only methods with validation annotations are shown here
    @NotEmpty(message = "{error.suggestion.titleEmpty}")
    @Size(min = 1, max = 155, message = "{error.suggestion.titleSize}")
    @Column(name="title_sug")
    public String getTitle() {
        return title;
    }
    // ...
}

En occurrence, il n'y a rien de compliqué. On implique deux validateurs : le premier (@NotEmpty) vérifie si la chaîne de caractères n'est pas vide. Le deuxième (@Size) s'assure que le texte saisi a entre 1 et 155 caractères.

Regardons maintenant l'entité Subscriber qui a plus de contraintes à valider :

public class Subscriber extends ParentEntity implements Serializable {
    // only validated method are shown here
    @NotEmpty(message = "{error.login.empty}", groups = SubscriberRegisterCheck.class)
    @Size(min = 1, max = 20, message = "{error.login.length}", groups = SubscriberRegisterCheck.class)
    @Pattern(regexp = "([A-Za-z0-9]+)", message = "{error.login.char}", groups = SubscriberRegisterCheck.class)
    @UniqueRecordDb(column = "login", query = "SELECT s FROM Subscriber s WHERE login = :login", parameter = "login", message = "{error.login.used}", groups = SubscriberRegisterCheck.class)
    @Column(name = "login_su")
    public String getLogin() {
        return login;
    }

    @NotEmpty(message = "{error.password.empty}", groups = {SubscriberRegisterCheck.class, SubscriberPasswordCheck.class})
    @Column(name = "password_su")
    public String getPassword() {
        return password;
    }

    @NotEmpty(message = "{error.email.empty}", groups = SubscriberRegisterCheck.class)
    @Email(message = "{error.email.format}", groups = SubscriberRegisterCheck.class)
    @UniqueRecordDb(column = "email", query = "SELECT s FROM Subscriber s WHERE email = :email", parameter = "email", message = "{error.email.used}", groups = SubscriberRegisterCheck.class)
    @Column(name = "email_su")
    public String getEmail() {
        return email;
    }
    @Transient
    @NotEmpty(message = "{error.repeatedPassword.empty}", groups = {SubscriberRegisterCheck.class, SubscriberPasswordCheck.class})
    public String getPasswordRepeated() {
        return passwordRepeated;
    } 
    
    // ...
}

Dans ce cas on voit plus de validations. Tout d'abord, on s'assure que ces éléments ne sont pas vides (@NotEmpty). Ensuite on vérifie, grâce à l'annotation @Email, que le format de l'attribut email est correct. Concernant le champ avec le login, on emploie un élément bien connu, @Size. Une nouvelle annotation rajoutée est @Pattern. Comme on peut facilement supposer, il s'agit d'une validation via le modèle basé sur une expression régulière. En occurrence, on veut vérifier si le login est composé de caractères alphanumériques. On remarque également la présence d'un nouvel attribut dans l'annotation de validation. Il s'agit du groups qui signifie les groups auxquels cette validation doit s'appliquer. On retrouve le même attribut dans le contrôleur utilisant @Validated.

Il est important de noter que deux annotations utilisées dans ces exemples, @NotEmpty et @Email, ne sont pas issues du package javax.validation.constraints. Les deux sont des éléments inclus dans Hibernate, dans son package de validation localisé dans org.hibernate.validator.constraints. Il suffit donc qu'on change de l'ORM pour qu'elles ne soient plus valides.

Bartosz KONIECZNY Validation

Une question ? Une remarque ?

*

*

Un conseil Symfony2

Comment générer un url absolu sous Symfony2 ?

La génération des urls sous Symfony2 se déroule via la méthode generateUrl() de la classe Symfony\Bundle\FrameworkBundle\Controller\Controller.

Cette fonction doit prendre un paramètre obligatoire - le premier qui indique le nom de la route à générer.

Les deux paramètres suivants sont facultatifs. Le premier parmi eux indique les attributs de l'url (par exemple le numéro de la page). Le deuxième prend pour valeur soit true, soit false. S'il est mis en true, cela veut dire que Symfony2 va générer l'url absolu. Cela s'illustre avec cet exemple :

$this->generateUrl('registerConfirm', array('code' => sha1('test')), true);