Validation des points de terminaison fonctionnels au printemps 5

Validation des points de terminaison fonctionnels au printemps 5

1. Vue d'ensemble

Il est souvent utile de mettre en œuvre une validation d'entrée pour nos API afin d'éviter des erreurs inattendues plus tard, lorsque nous traitons les données.

Malheureusement, dans Spring 5, il n’existe aucun moyen d’exécuter automatiquement des validations sur les points de terminaison fonctionnels comme nous le faisons sur les points de terminaison annotés. Nous devons les gérer manuellement.

Néanmoins, nous pouvons utiliser certains outils utiles fournis par Spring pour vérifier facilement et proprement que nos ressources sont valides.

2. Utiliser les validations de printemps

Commençons par configurer notre projet avec un point de terminaison fonctionnel avant de plonger dans les validations réelles.

Imaginons que nous ayons lesRouterFunction suivants:

@Bean
public RouterFunction functionalRoute(
  FunctionalHandler handler) {
    return RouterFunctions.route(
      RequestPredicates.POST("/functional-endpoint"),
      handler::handleRequest);
}

Ce routeur utilise la fonction de gestionnaire fournie par la classe de contrôleur suivante:

@Component
public class FunctionalHandler {

    public Mono handleRequest(ServerRequest request) {
        Mono responseBody = request
          .bodyToMono(CustomRequestEntity.class)
          .map(cre -> String.format(
            "Hi, %s [%s]!", cre.getName(), cre.getCode()));

        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(responseBody, String.class);
    }
}

Comme nous pouvons le voir, tout ce que nous faisons dans ce point de terminaison fonctionnel est de formater et de récupérer les informations que nous avons reçues dans le corps de la requête, qui est structuré comme un objetCustomRequestEntity:

public class CustomRequestEntity {

    private String name;
    private String code;

    // ... Constructors, Getters and Setters ...

}

Cela fonctionne très bien, mais imaginons que nous devons maintenant vérifier que notre entrée est conforme à certaines contraintes données, par exemple, qu'aucun des champs ne peut être nul et que le code doit comporter plus de 6 chiffres.

Nous devons trouver un moyen de formuler ces affirmations efficacement et, si possible, en les dissociant de notre logique métier.

2.1. Implémentation d'un validateur

As it’s explained in this Spring Reference Documentation, we can use the Spring’s Validator interface to evaluate our resource’s values:

public class CustomRequestEntityValidator
  implements Validator {

    @Override
    public boolean supports(Class clazz) {
        return CustomRequestEntity.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "name", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "code", "field.required");
        CustomRequestEntity request = (CustomRequestEntity) target;
        if (request.getCode() != null && request.getCode().trim().length() < 6) {
            errors.rejectValue(
              "code",
              "field.min.length",
              new Object[] { Integer.valueOf(6) },
              "The code must be at least [6] characters in length.");
        }
    }
}

Nous n'entrerons pas dans les détails surhow le fonctionnement deValidator. Il suffit de savoir que toutes les erreurs sont collectées lors de la validation d’un objet -an empty error collection means that the object adheres to all our constraints.

Alors maintenant que nous avons nosValidator en place, nous devrons l'appeler explicitementvalidate  avant d'exécuter réellement notre logique métier.

2.2. Exécuter les validations

Dans un premier temps, on peut penser que l'utilisation d'unHandlerFilterFunction conviendrait dans notre situation.

Mais nous devons garder à l'esprit que dans ces filtres - identiques à ceux des gestionnaires - nous traitonsasynchronous constructions - tels queMono etFlux.

Cela signifie que nous aurons accès auxPublisher (lesMono ou l’objetFlux) mais pas aux données qu’il fournira éventuellement.

Par conséquent, la meilleure chose que nous puissions faire est de valider le corps lorsque nous le traitons réellement dans la fonction de gestionnaire.

Allons-y et modifions notre méthode de gestionnaire, y compris la logique de validation:

public Mono handleRequest(ServerRequest request) {
    Validator validator = new CustomRequestEntityValidator();
    Mono responseBody = request
      .bodyToMono(CustomRequestEntity.class)
      .map(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          CustomRequestEntity.class.getName());
        validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return String.format("Hi, %s [%s]!", body.getName(), body.getCode());
        } else {
            throw new ResponseStatusException(
              HttpStatus.BAD_REQUEST,
              errors.getAllErrors().toString());
        }
    });
    return ServerResponse.ok()
      .contentType(MediaType.APPLICATION_JSON)
      .body(responseBody, String.class);
}

En un mot, notre service récupérera désormais une réponse «Bad Request» si le corps de la requête n'est pas conforme à nos restrictions.

Pouvons-nous dire que nous avons atteint notre objectif? Eh bien, nous y sommes presque. We’re running the validations, but there are many drawbacks in this approach.

Nous mélangons les validations avec la logique métier et, pour aggraver les choses, nous devrons répéter le code ci-dessus dans tout gestionnaire sur lequel nous voulons effectuer notre validation d'entrée.

Essayons d’améliorer cela.

3. Travailler sur une approche sèche

To create a cleaner solution we’ll start by declaring an abstract class containing the basic procedure to process a request.

Tous les gestionnaires qui ont besoin d'une validation d'entrée étendront cette classe abstraite, afin de réutiliser son schéma principal, et donc en suivant le principe DRY (ne pas se répéter).

Nous utiliserons des génériques afin de le rendre suffisamment flexible pour prendre en charge tout type de corps et son validateur respectif:

public abstract class AbstractValidationHandler {

    private final Class validationClass;

    private final U validator;

    protected AbstractValidationHandler(Class clazz, U validator) {
        this.validationClass = clazz;
        this.validator = validator;
    }

    public final Mono handleRequest(final ServerRequest request) {
        // ...here we will validate and process the request...
    }
}

Codons maintenant notre méthodehandleRequest avec la procédure standard:

public Mono handleRequest(final ServerRequest request) {
    return request.bodyToMono(this.validationClass)
      .flatMap(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          this.validationClass.getName());
        this.validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return processBody(body, request);
        } else {
            return onValidationErrors(errors, body, request);
        }
    });
}

Comme nous pouvons le voir, nous utilisons deux méthodes que nous n'avons pas encore créées.

Définissons celui qui est invoqué lorsque nous avons d’abord des erreurs de validation:

protected Mono onValidationErrors(
  Errors errors,
  T invalidBody,
  ServerRequest request) {
    throw new ResponseStatusException(
      HttpStatus.BAD_REQUEST,
      errors.getAllErrors().toString());
}

Ceci est juste une implémentation par défaut, cependant, elle peut être facilement remplacée par les classes enfants.

Finally, we’ll set the processBody method undefined -we’ll leave it up to the child classes to determine how to proceed in that case:

abstract protected Mono processBody(
  T validBody,
  ServerRequest originalRequest);

Il y a quelques aspects à analyser dans cette classe.

Tout d’abord, en utilisant des génériques, les implémentations enfants devront déclarer explicitement le type de contenu attendu et le validateur qui sera utilisé pour l’évaluer.

Cela rend également notre structure robuste, car elle limite les signatures de nos méthodes.

Au moment de l'exécution, le constructeur affectera l'objet de validation actuel et la classe utilisée pour transtyper le corps de la demande.

Nous pouvons jeter un œil à la classe complètehere.

Voyons maintenant comment nous pouvons bénéficier de cette structure.

3.1. Adapter notre gestionnaire

La première chose que nous devrons faire, évidemment, est d'étendre notre gestionnaire à partir de cette classe abstraite.

En faisant cela,we’ll be forced to use the parent’s constructor and to define how we’ll handle our request in the processBody method:

@Component
public class FunctionalHandler
  extends AbstractValidationHandler {

    private CustomRequestEntityValidationHandler() {
        super(CustomRequestEntity.class, new CustomRequestEntityValidator());
    }

    @Override
    protected Mono processBody(
      CustomRequestEntity validBody,
      ServerRequest originalRequest) {
        String responseBody = String.format(
          "Hi, %s [%s]!",
          validBody.getName(),
          validBody.getCode());
        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(Mono.just(responseBody), String.class);
    }
}

Comme nous pouvons l'apprécier, notre gestionnaire d'enfant est maintenant beaucoup plus simple que celui que nous avons obtenu dans la section précédente, car il évite de jouer avec la validation réelle des ressources.

4. Prise en charge des annotations d'API de validation de bean

Avec cette approche, nous pouvons également profiter des puissantsBean Validation’s annotations fournis par le packagejavax.validation.

Par exemple, définissons une nouvelle entité avec des champs annotés:

public class AnnotatedRequestEntity {

    @NotNull
    private String user;

    @NotNull
    @Size(min = 4, max = 7)
    private String password;

    // ... Constructors, Getters and Setters ...
}

We can now simply create a new handler injected with the default Spring Validator provided by the LocalValidatorFactoryBean bean:

public class AnnotatedRequestEntityValidationHandler
  extends AbstractValidationHandler {

    private AnnotatedRequestEntityValidationHandler(@Autowired Validator validator) {
        super(AnnotatedRequestEntity.class, validator);
    }

    @Override
    protected Mono processBody(
      AnnotatedRequestEntity validBody,
      ServerRequest originalRequest) {

        // ...

    }
}

Nous devons garder à l'esprit que s'il y a d'autres beansValidator présents dans le contexte, nous pourrions avoir à déclarer explicitement celui-ci avec l'annotation@Primary:

@Bean
@Primary
public Validator springValidator() {
    return new LocalValidatorFactoryBean();
}

5. Conclusion

Pour résumer, dans cet article, nous avons appris à valider les données d'entrée dans les points de terminaison fonctionnels Spring 5.

Nous avons créé une belle approche pour gérer les validations avec élégance en évitant de mêler sa logique à celle des entreprises.

Bien entendu, la solution suggérée peut ne pas convenir à n'importe quel scénario. Nous devrons analyser notre situation et probablement adapter la structure à nos besoins.

Si nous voulons voir tout l'exemple de travail, nous pouvons le trouver dansour GitHub repo.