in spring validation hibernate validator jsr 303 bean validation ~ de lecture.

Valider ses POJOs avec Spring Bean Validation

Il est indispensable de vérifier côté serveur les données saisies par vos utilisateurs, ou bien vérifier que les données retournées par votre API ont un sens et sont correctement formatées.

Les annotations @Valid ou @Validated de Spring MVC au niveau d'un @Controller permettent de déclencher automatiquement la validation de vos beans à l'entrée et d'en récupérer le résultat via BindingResult.
Cependant il est parfois utile de déclencher cette validation manuellement, en dehors de tout contexte, c'est ce qu'il va être développé dans la suite du billet.

Spring Validation

Déclenchement manuel

Vous souhaitez valider un ou plusieurs objets simples (POJO) et vous n'avez pas la main sur ces objets, dans ce cas il faut vous créer une classe de validation qui va appliquer manuellement les contraintes souhaitées.

Exemple : un produit doit avoir une marque, une description et un prix correct :

public class Product {

    private String description;
    private String brand;
    private Price price;

    // getters & setters
}

public class Price {

    private String type;
    private int value;
    private String currency;

    // getters & setters
}

Exemple simpliste d'ajout de contraintes explicites sur l'objet Product :

@Component
public class ProductValidator {

    @Autowired
    private PriceValidator priceValidator;
    
    public Product validate(Product product) {
        if (product == null) {
            return product;
        }

        if (product.getDescription() == null || product.getDescription().length() < 5) {
            throw new IllegalArgumentException("Product has no valid description");
        }

        if (product.getBrand() == null) {
            throw new IllegalArgumentException("Product has no valid brand");
        }

        if (product.getPrice() == null) {
            throw new IllegalArgumentException("Product has no valid price");
        } else {
            priceValidator.validate(product.getPrice());
        }

        return product;
    }
}

@Component
public class PriceValidator {

    public Price validate(Price price) {
        if (price == null) {
            return price;
        }

        if (price.getType() == null) {
            throw new IllegalArgumentException("Price has no valid type");
        }

        if (price.getValue() < 0) {
            throw new IllegalArgumentException("Price has no valid value");
        }

        if (price.getCurrency() == null) {
            throw new IllegalArgumentException("Price has no valid currency");
        }

        return price;
    }
}

A première vue c'est une façon classique de procéder, l'avantage c'est que tout peut être explicitement contrôlé, l'inconvénient est que tout est manuel et qu'une classe de validation est nécessaire pour chaque POJO à valider.

Déclenchement semi-automatique (JSR-303)

Dans le cas contraire où vous avez la main sur les objets à valider, il est possible de procéder différemment et de décrire les contraintes directement au niveau des attributs du POJO en utilisant les annotations de type "javax.validation".

Pour cela nous avons besoin d'instancier un ValidatorFactoryBean, de l'injecter où bon nous semble et de l'utiliser correctement afin de récupérer le résultat de la validation Spring.

  • Bibliothèques nécessaires
<dependency>
      <groupId>javax.validation</groupId>
      <artifactId>validation-api</artifactId>
      <version>1.1.0.Final</version>
</dependency>

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.4.0.Final</version>
</dependency>
  • Configuration Spring
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;

@Configuration
public class ValidationConfiguration {

    @Bean
    public SpringValidatorAdapter validator() {
        return new LocalValidatorFactoryBean();
    }
}
  • POJOs avec contraintes : noter l'annotation @Valid sur l'attribut price pour que les contraintes du POJO Price se déclenchent aussi lorsque l'objet Product est validé
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

public class Product {

    @Length(min = 5)
    private String description;

    @NotBlank
    private String brand;

    @NotNull
    @Valid
    private Price price;

    // getters & setters
}
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

public class Price {

    @NotNull
    private String type;

    @Min(1)
    private int value;

    @Pattern(regexp = ".+")
    private String currency;

    // getters & setters
}
  • Cas d'utilisation : écriture d'une classe générique que vous pourrez injecter et utiliser où vous en avez besoin
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
import org.springframework.validation.Validator;

@Component
public class BeanValidator {

    @Autowired
    private Validator validator;

    public <T> T validate(T target) {
        if (target == null) {
            return target;
        }

        DataBinder binder = new DataBinder(target);
        BindingResult results = binder.getBindingResult();
        validator.validate(target, results);

        // results contains validation errors
        // do what you want with them (exception, log ...)

        return target;
    }
}

Cette façon de procéder a l'avantage d'être plus lisible (plus maintenable ?) et de n'avoir qu'une seule classe de validation à écrire (elle est générique).

C'est une affaire d'opportunité à mon sens, la deuxième option est nettement plus élégante.